{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 第三阶段 - 第4讲：数据格式规范与去重\n",
    "\n",
    "## 学习目标\n",
    "- 掌握数据类型转换的各种方法\n",
    "- 熟练处理日期时间数据（解析、提取、运算）\n",
    "- 掌握字符串处理的30+种方法\n",
    "- 学会使用正则表达式清洗文本数据\n",
    "- 掌握去重的多种策略\n",
    "- 完成数据标准化和规范化\n",
    "\n",
    "**重要性**: ⭐⭐⭐⭐ 数据清洗的最后一步，确保数据格式统一！\n",
    "\n",
    "---"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✅ 环境配置完成\n",
      "Pandas版本: 2.2.3\n"
     ]
    }
   ],
   "source": [
    "# 导入必要的库\n",
    "import pandas as pd\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "import seaborn as sns\n",
    "from datetime import datetime, timedelta\n",
    "import re  # 正则表达式\n",
    "import warnings\n",
    "warnings.filterwarnings('ignore')\n",
    "\n",
    "# 设置\n",
    "pd.set_option('display.max_columns', None)\n",
    "pd.set_option('display.max_rows', 100)\n",
    "pd.set_option('display.float_format', '{:.2f}'.format)\n",
    "\n",
    "# 中文显示\n",
    "plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'SimHei']\n",
    "plt.rcParams['axes.unicode_minus'] = False\n",
    "\n",
    "print(\"✅ 环境配置完成\")\n",
    "print(f\"Pandas版本: {pd.__version__}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 一、数据类型转换\n",
    "\n",
    "### 1.1 为什么要关注数据类型？\n",
    "\n",
    "**重要性**:\n",
    "- 影响内存占用\n",
    "- 影响计算性能\n",
    "- 影响函数可用性\n",
    "- 影响数据准确性\n",
    "\n",
    "**常见问题**:\n",
    "- 数字被识别为字符串 → 无法计算\n",
    "- 日期被识别为字符串 → 无法排序和筛选\n",
    "- 类型不匹配 → 合并/比较出错\n",
    "\n",
    "### 1.2 Pandas主要数据类型\n",
    "\n",
    "| 类型 | 说明 | 示例 |\n",
    "|------|------|------|\n",
    "| `int64` | 整数 | 1, 100, -5 |\n",
    "| `float64` | 浮点数 | 3.14, -0.5 |\n",
    "| `object` | 字符串或混合 | 'abc', 'hello' |\n",
    "| `bool` | 布尔值 | True, False |\n",
    "| `datetime64` | 日期时间 | 2024-01-01 |\n",
    "| `category` | 分类 | '男', '女' |\n",
    "| `timedelta` | 时间差 | 3 days |"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "原始数据:\n",
      "  id name      age   salary    join_date is_active  score\n",
      "0  1   张三       28     8000   2020-01-15       yes     85\n",
      "1  2   李四       35  12000.5   2019/03/20        no     92\n",
      "2  3   王五       42    15000  15-Jun-2021       yes     88\n",
      "3  4   赵六  unknown    10000   2022.05.10       yes     95\n",
      "4  5   钱七       31    9,500     20230815        no     90\n",
      "\n",
      "数据类型:\n",
      "id           object\n",
      "name         object\n",
      "age          object\n",
      "salary       object\n",
      "join_date    object\n",
      "is_active    object\n",
      "score         int64\n",
      "dtype: object\n",
      "\n",
      "问题: 很多应该是数字的列被识别为object(字符串)\n"
     ]
    }
   ],
   "source": [
    "# 创建示例数据 - 包含类型问题\n",
    "messy_types = {\n",
    "    'id': ['1', '2', '3', '4', '5'],\n",
    "    'name': ['张三', '李四', '王五', '赵六', '钱七'],\n",
    "    'age': ['28', '35', '42', 'unknown', '31'],\n",
    "    'salary': ['8000', '12000.5', '15000', '10000', '9,500'],\n",
    "    'join_date': ['2020-01-15', '2019/03/20', '15-Jun-2021', '2022.05.10', '20230815'],\n",
    "    'is_active': ['yes', 'no', 'yes', 'yes', 'no'],\n",
    "    'score': [85, 92, 88, 95, 90]\n",
    "}\n",
    "\n",
    "df_types = pd.DataFrame(messy_types)\n",
    "\n",
    "print(\"原始数据:\")\n",
    "print(df_types)\n",
    "print(\"\\n数据类型:\")\n",
    "print(df_types.dtypes)\n",
    "print(\"\\n问题: 很多应该是数字的列被识别为object(字符串)\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1.3 类型转换方法\n",
    "\n",
    "**Excel对比**:\n",
    "- Excel: 手动设置单元格格式，或用VALUE()、TEXT()函数\n",
    "- Pandas: `astype()`, `to_numeric()`, `to_datetime()`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== astype()类型转换 ===\n",
      "1. id转换为int: int64\n",
      "2. score转换为str: object\n",
      "\n",
      "转换后的数据类型:\n",
      "id            int64\n",
      "name         object\n",
      "age          object\n",
      "salary       object\n",
      "join_date    object\n",
      "is_active    object\n",
      "score         int64\n",
      "score_str    object\n",
      "dtype: object\n"
     ]
    }
   ],
   "source": [
    "# 方法1: astype() - 最基础的类型转换\n",
    "df_convert = df_types.copy()\n",
    "\n",
    "print(\"=== astype()类型转换 ===\")\n",
    "\n",
    "# 转换id为整数\n",
    "df_convert['id'] = df_convert['id'].astype(int)\n",
    "print(f\"1. id转换为int: {df_convert['id'].dtype}\")\n",
    "\n",
    "# 转换score为字符串\n",
    "df_convert['score_str'] = df_convert['score'].astype(str)\n",
    "print(f\"2. score转换为str: {df_convert['score_str'].dtype}\")\n",
    "\n",
    "print(\"\\n转换后的数据类型:\")\n",
    "print(df_convert.dtypes)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== to_numeric()智能转换 ===\n",
      "直接转换出错: invalid literal for int() with base 10: 'unknown'\n",
      "\n",
      "to_numeric转换结果:\n",
      "       age  age_numeric\n",
      "0       28        28.00\n",
      "1       35        35.00\n",
      "2       42        42.00\n",
      "3  unknown          NaN\n",
      "4       31        31.00\n",
      "\n",
      "errors='ignore'结果:\n",
      "       age age_ignore\n",
      "0       28         28\n",
      "1       35         35\n",
      "2       42         42\n",
      "3  unknown    unknown\n",
      "4       31         31\n"
     ]
    }
   ],
   "source": [
    "# 方法2: to_numeric() - 智能数字转换\n",
    "print(\"\\n=== to_numeric()智能转换 ===\")\n",
    "\n",
    "# 问题: age列有'unknown'，直接astype会报错\n",
    "try:\n",
    "    df_convert['age'].astype(int)\n",
    "except ValueError as e:\n",
    "    print(f\"直接转换出错: {e}\")\n",
    "\n",
    "# 使用to_numeric处理\n",
    "# errors='coerce': 无法转换的变为NaN\n",
    "df_convert['age_numeric'] = pd.to_numeric(df_convert['age'], errors='coerce')\n",
    "print(f\"\\nto_numeric转换结果:\")\n",
    "print(df_convert[['age', 'age_numeric']])\n",
    "\n",
    "# errors='ignore': 无法转换的保持原样\n",
    "df_convert['age_ignore'] = pd.to_numeric(df_convert['age'], errors='ignore')\n",
    "print(f\"\\nerrors='ignore'结果:\")\n",
    "print(df_convert[['age', 'age_ignore']])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== 处理特殊格式数字 ===\n",
      "\n",
      "原始salary:\n",
      "0       8000\n",
      "1    12000.5\n",
      "2      15000\n",
      "3      10000\n",
      "4      9,500\n",
      "Name: salary, dtype: object\n",
      "\n",
      "清洗后的salary:\n",
      "    salary  salary_clean\n",
      "0     8000       8000.00\n",
      "1  12000.5      12000.50\n",
      "2    15000      15000.00\n",
      "3    10000      10000.00\n",
      "4    9,500       9500.00\n",
      "\n",
      "类型: float64\n"
     ]
    }
   ],
   "source": [
    "# 方法3: 处理特殊格式的数字\n",
    "print(\"=== 处理特殊格式数字 ===\")\n",
    "\n",
    "# salary列有逗号分隔符\n",
    "print(\"\\n原始salary:\")\n",
    "print(df_convert['salary'])\n",
    "\n",
    "# 先去除逗号，再转换\n",
    "df_convert['salary_clean'] = df_convert['salary'].str.replace(',', '').astype(float)\n",
    "print(\"\\n清洗后的salary:\")\n",
    "print(df_convert[['salary', 'salary_clean']])\n",
    "\n",
    "print(f\"\\n类型: {df_convert['salary_clean'].dtype}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== 布尔值转换 ===\n",
      "  is_active  is_active_bool\n",
      "0       yes            True\n",
      "1        no           False\n",
      "2       yes            True\n",
      "3       yes            True\n",
      "4        no           False\n",
      "\n",
      "类型: bool\n"
     ]
    }
   ],
   "source": [
    "# 方法4: 布尔值转换\n",
    "print(\"=== 布尔值转换 ===\")\n",
    "\n",
    "# 定义映射\n",
    "bool_mapping = {'yes': True, 'no': False}\n",
    "df_convert['is_active_bool'] = df_convert['is_active'].map(bool_mapping)\n",
    "\n",
    "print(df_convert[['is_active', 'is_active_bool']])\n",
    "print(f\"\\n类型: {df_convert['is_active_bool'].dtype}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== category类型 ===\n",
      "object类型内存: 517 bytes\n",
      "category类型内存: 694 bytes\n",
      "节省: -34.2%\n"
     ]
    }
   ],
   "source": [
    "# 方法5: category类型 - 优化内存\n",
    "print(\"=== category类型 ===\")\n",
    "\n",
    "# 对于重复值多的列，使用category可以节省内存\n",
    "df_convert['name_cat'] = df_convert['name'].astype('category')\n",
    "\n",
    "print(f\"object类型内存: {df_convert['name'].memory_usage(deep=True)} bytes\")\n",
    "print(f\"category类型内存: {df_convert['name_cat'].memory_usage(deep=True)} bytes\")\n",
    "print(f\"节省: {(1 - df_convert['name_cat'].memory_usage(deep=True)/df_convert['name'].memory_usage(deep=True))*100:.1f}%\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1.4 批量类型转换"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== 批量类型转换 ===\n",
      "\n",
      "转换前:\n",
      "int_col      object\n",
      "float_col    object\n",
      "cat_col      object\n",
      "dtype: object\n",
      "总内存: 15332 bytes\n",
      "\n",
      "转换后:\n",
      "int_col         int32\n",
      "float_col     float32\n",
      "cat_col      category\n",
      "dtype: object\n",
      "总内存: 1290 bytes\n",
      "\n",
      "内存优化显著！\n"
     ]
    }
   ],
   "source": [
    "# 创建更大的数据集\n",
    "large_data = {\n",
    "    'int_col': ['1', '2', '3', '4', '5'] * 20,\n",
    "    'float_col': ['1.5', '2.3', '3.7', '4.2', '5.8'] * 20,\n",
    "    'cat_col': ['A', 'B', 'C', 'A', 'B'] * 20\n",
    "}\n",
    "df_large = pd.DataFrame(large_data)\n",
    "\n",
    "print(\"=== 批量类型转换 ===\")\n",
    "print(\"\\n转换前:\")\n",
    "print(df_large.dtypes)\n",
    "print(f\"总内存: {df_large.memory_usage(deep=True).sum()} bytes\")\n",
    "\n",
    "# 批量转换\n",
    "df_large = df_large.astype({\n",
    "    'int_col': 'int32',      # 用int32而不是int64节省内存\n",
    "    'float_col': 'float32',  # 用float32而不是float64\n",
    "    'cat_col': 'category'    # 分类型\n",
    "})\n",
    "\n",
    "print(\"\\n转换后:\")\n",
    "print(df_large.dtypes)\n",
    "print(f\"总内存: {df_large.memory_usage(deep=True).sum()} bytes\")\n",
    "print(f\"\\n内存优化显著！\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 二、日期时间处理\n",
    "\n",
    "### 2.1 为什么日期处理很重要？\n",
    "\n",
    "**业务场景**:\n",
    "- 时间序列分析\n",
    "- 计算时间差（用户留存、订单周期）\n",
    "- 按时间分组（日/周/月/季度/年）\n",
    "- 工作日/节假日判断\n",
    "\n",
    "**常见问题**:\n",
    "- 日期格式不统一\n",
    "- 字符串无法排序和计算\n",
    "- 时区问题\n",
    "\n",
    "### 2.2 日期解析 - to_datetime()\n",
    "\n",
    "**Excel对比**:\n",
    "- Excel: 日期序列值，或用DATEVALUE()函数\n",
    "- Pandas: `pd.to_datetime()` 自动识别多种格式"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== 日期解析 ===\n",
      "\n",
      "原始join_date（多种格式）:\n",
      "0     2020-01-15\n",
      "1     2019/03/20\n",
      "2    15-Jun-2021\n",
      "3     2022.05.10\n",
      "4       20230815\n",
      "Name: join_date, dtype: object\n",
      "类型: object\n"
     ]
    }
   ],
   "source": [
    "# 日期解析示例\n",
    "print(\"=== 日期解析 ===\")\n",
    "\n",
    "# 回到我们的数据\n",
    "print(\"\\n原始join_date（多种格式）:\")\n",
    "print(df_convert['join_date'])\n",
    "print(f\"类型: {df_convert['join_date'].dtype}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "解析后:\n",
      "     join_date join_date_parsed\n",
      "0   2020-01-15       2020-01-15\n",
      "1   2019/03/20              NaT\n",
      "2  15-Jun-2021              NaT\n",
      "3   2022.05.10              NaT\n",
      "4     20230815              NaT\n",
      "\n",
      "类型: datetime64[ns]\n"
     ]
    }
   ],
   "source": [
    "# 自动解析（Pandas很智能！）\n",
    "df_convert['join_date_parsed'] = pd.to_datetime(df_convert['join_date'], errors='coerce')\n",
    "\n",
    "print(\"\\n解析后:\")\n",
    "print(df_convert[['join_date', 'join_date_parsed']])\n",
    "print(f\"\\n类型: {df_convert['join_date_parsed'].dtype}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== 各种格式解析测试 ===\n",
      "标准格式         2024-01-15           → 2024-01-15 00:00:00\n",
      "斜杠分隔         2024/01/15           → 2024-01-15 00:00:00\n",
      "点分隔          2024.01.15           → 2024-01-15 00:00:00\n",
      "中文           2024年1月15日           → 解析失败\n",
      "无分隔          20240115             → 2024-01-15 00:00:00\n",
      "美式           01/15/2024           → 2024-01-15 00:00:00\n",
      "欧式           15/01/2024           → 2024-01-15 00:00:00\n",
      "月份名          15-Jan-2024          → 2024-01-15 00:00:00\n",
      "时间戳          1705276800           → 2024-01-15 00:00:00\n",
      "带时间          2024-01-15 14:30:00  → 2024-01-15 14:30:00\n"
     ]
    }
   ],
   "source": [
    "# 各种日期格式的解析\n",
    "date_formats = {\n",
    "    '标准格式': '2024-01-15',\n",
    "    '斜杠分隔': '2024/01/15',\n",
    "    '点分隔': '2024.01.15',\n",
    "    '中文': '2024年1月15日',\n",
    "    '无分隔': '20240115',\n",
    "    '美式': '01/15/2024',\n",
    "    '欧式': '15/01/2024',\n",
    "    '月份名': '15-Jan-2024',\n",
    "    '时间戳': '1705276800',\n",
    "    '带时间': '2024-01-15 14:30:00'\n",
    "}\n",
    "\n",
    "print(\"\\n=== 各种格式解析测试 ===\")\n",
    "for name, date_str in date_formats.items():\n",
    "    try:\n",
    "        if name == '时间戳':\n",
    "            parsed = pd.to_datetime(int(date_str), unit='s')\n",
    "        else:\n",
    "            parsed = pd.to_datetime(date_str)\n",
    "        print(f\"{name:12} {date_str:20} → {parsed}\")\n",
    "    except:\n",
    "        print(f\"{name:12} {date_str:20} → 解析失败\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== 指定格式解析 ===\n",
      "自动推断:\n",
      "0   2024-01-15\n",
      "1   2024-03-20\n",
      "2   2024-06-25\n",
      "dtype: datetime64[ns]\n",
      "\n",
      "指定格式:\n",
      "0   2024-01-15\n",
      "1   2024-03-20\n",
      "2   2024-06-25\n",
      "dtype: datetime64[ns]\n"
     ]
    }
   ],
   "source": [
    "# 指定格式解析（更快）\n",
    "print(\"\\n=== 指定格式解析 ===\")\n",
    "\n",
    "# 创建示例\n",
    "dates_specific = pd.Series(['15/01/2024', '20/03/2024', '25/06/2024'])\n",
    "\n",
    "# 方式1: 自动推断（慢）\n",
    "parsed_auto = pd.to_datetime(dates_specific, dayfirst=True)\n",
    "print(\"自动推断:\")\n",
    "print(parsed_auto)\n",
    "\n",
    "# 方式2: 指定格式（快）\n",
    "parsed_format = pd.to_datetime(dates_specific, format='%d/%m/%Y')\n",
    "print(\"\\n指定格式:\")\n",
    "print(parsed_format)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.3 日期提取\n",
    "\n",
    "**从日期中提取各种信息**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== 日期提取 ===\n",
      "\n",
      "提取的日期信息:\n",
      "        date  year  month  day  quarter  week  weekday   day_name  is_weekend  \\\n",
      "0 2024-01-01  2024      1    1        1     1        0     Monday       False   \n",
      "1 2024-01-02  2024      1    2        1     1        1    Tuesday       False   \n",
      "2 2024-01-03  2024      1    3        1     1        2  Wednesday       False   \n",
      "3 2024-01-04  2024      1    4        1     1        3   Thursday       False   \n",
      "4 2024-01-05  2024      1    5        1     1        4     Friday       False   \n",
      "5 2024-01-06  2024      1    6        1     1        5   Saturday        True   \n",
      "6 2024-01-07  2024      1    7        1     1        6     Sunday        True   \n",
      "7 2024-01-08  2024      1    8        1     2        0     Monday       False   \n",
      "8 2024-01-09  2024      1    9        1     2        1    Tuesday       False   \n",
      "9 2024-01-10  2024      1   10        1     2        2  Wednesday       False   \n",
      "\n",
      "   is_month_start  is_month_end  day_of_year  \n",
      "0            True         False            1  \n",
      "1           False         False            2  \n",
      "2           False         False            3  \n",
      "3           False         False            4  \n",
      "4           False         False            5  \n",
      "5           False         False            6  \n",
      "6           False         False            7  \n",
      "7           False         False            8  \n",
      "8           False         False            9  \n",
      "9           False         False           10  \n",
      "\n",
      "周末天数:\n",
      "周末: 28天\n",
      "工作日: 72天\n"
     ]
    }
   ],
   "source": [
    "# 创建完整的日期数据\n",
    "date_data = pd.DataFrame({\n",
    "    'date': pd.date_range('2024-01-01', periods=100, freq='D')\n",
    "})\n",
    "\n",
    "print(\"=== 日期提取 ===\")\n",
    "\n",
    "# 提取年月日\n",
    "date_data['year'] = date_data['date'].dt.year\n",
    "date_data['month'] = date_data['date'].dt.month\n",
    "date_data['day'] = date_data['date'].dt.day\n",
    "\n",
    "# 提取季度和周\n",
    "date_data['quarter'] = date_data['date'].dt.quarter\n",
    "date_data['week'] = date_data['date'].dt.isocalendar().week\n",
    "\n",
    "# 星期几\n",
    "date_data['weekday'] = date_data['date'].dt.weekday  # 0=周一\n",
    "date_data['day_name'] = date_data['date'].dt.day_name()  # 英文名称\n",
    "\n",
    "# 是否周末\n",
    "date_data['is_weekend'] = date_data['weekday'].isin([5, 6])\n",
    "\n",
    "# 月初月末\n",
    "date_data['is_month_start'] = date_data['date'].dt.is_month_start\n",
    "date_data['is_month_end'] = date_data['date'].dt.is_month_end\n",
    "\n",
    "# 一年中的第几天\n",
    "date_data['day_of_year'] = date_data['date'].dt.dayofyear\n",
    "\n",
    "print(\"\\n提取的日期信息:\")\n",
    "print(date_data.head(10))\n",
    "\n",
    "print(\"\\n周末天数:\")\n",
    "print(f\"周末: {date_data['is_weekend'].sum()}天\")\n",
    "print(f\"工作日: {(~date_data['is_weekend']).sum()}天\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.4 日期运算\n",
    "\n",
    "**计算时间差、日期偏移**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== 日期运算 ===\n",
      "\n",
      "员工日期数据:\n",
      "  name  hire_date leave_date\n",
      "0   张三 2020-01-15 2023-12-31\n",
      "1   李四 2019-06-20        NaT\n",
      "2   王五 2021-03-10        NaT\n",
      "3   赵六 2018-09-05 2024-01-15\n",
      "4   钱七 2022-11-20        NaT\n"
     ]
    }
   ],
   "source": [
    "# 创建示例：员工入职和离职数据\n",
    "employee_dates = pd.DataFrame({\n",
    "    'name': ['张三', '李四', '王五', '赵六', '钱七'],\n",
    "    'hire_date': pd.to_datetime(['2020-01-15', '2019-06-20', '2021-03-10', '2018-09-05', '2022-11-20']),\n",
    "    'leave_date': pd.to_datetime(['2023-12-31', None, None, '2024-01-15', None])\n",
    "})\n",
    "\n",
    "print(\"=== 日期运算 ===\")\n",
    "print(\"\\n员工日期数据:\")\n",
    "print(employee_dates)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "在职时长:\n",
      "  name  hire_date leave_date  days_employed  years_employed\n",
      "0   张三 2020-01-15 2023-12-31           1446            4.00\n",
      "1   李四 2019-06-20        NaT           1656            4.50\n",
      "2   王五 2021-03-10        NaT           1027            2.80\n",
      "3   赵六 2018-09-05 2024-01-15           1958            5.40\n",
      "4   钱七 2022-11-20        NaT            407            1.10\n"
     ]
    }
   ],
   "source": [
    "# 计算在职天数\n",
    "today = pd.Timestamp('2024-01-01')\n",
    "\n",
    "# 对于离职的用离职日期，否则用今天\n",
    "employee_dates['end_date'] = employee_dates['leave_date'].fillna(today)\n",
    "employee_dates['days_employed'] = (employee_dates['end_date'] - employee_dates['hire_date']).dt.days\n",
    "\n",
    "# 计算年数\n",
    "employee_dates['years_employed'] = (employee_dates['days_employed'] / 365.25).round(1)\n",
    "\n",
    "print(\"\\n在职时长:\")\n",
    "print(employee_dates[['name', 'hire_date', 'leave_date', 'days_employed', 'years_employed']])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== 日期偏移 ===\n",
      "基准日期: 2024-01-15 00:00:00\n",
      "\n",
      "+7天: 2024-01-22 00:00:00\n",
      "-30天: 2023-12-16 00:00:00\n",
      "+1月: 2024-02-15 00:00:00\n",
      "+1年: 2025-01-15 00:00:00\n",
      "\n",
      "本月第一天: 2024-02-01 00:00:00\n",
      "本月最后一天: 2024-01-31 00:00:00\n",
      "下月第一天: 2024-02-01 00:00:00\n"
     ]
    }
   ],
   "source": [
    "# 日期偏移\n",
    "print(\"\\n=== 日期偏移 ===\")\n",
    "\n",
    "base_date = pd.Timestamp('2024-01-15')\n",
    "print(f\"基准日期: {base_date}\")\n",
    "\n",
    "# 加减天数\n",
    "print(f\"\\n+7天: {base_date + pd.Timedelta(days=7)}\")\n",
    "print(f\"-30天: {base_date - pd.Timedelta(days=30)}\")\n",
    "\n",
    "# 加减月份（使用DateOffset）\n",
    "print(f\"+1月: {base_date + pd.DateOffset(months=1)}\")\n",
    "print(f\"+1年: {base_date + pd.DateOffset(years=1)}\")\n",
    "\n",
    "# 到月初/月末\n",
    "print(f\"\\n本月第一天: {base_date + pd.offsets.MonthBegin(0)}\")\n",
    "print(f\"本月最后一天: {base_date + pd.offsets.MonthEnd(0)}\")\n",
    "print(f\"下月第一天: {base_date + pd.offsets.MonthBegin(1)}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== 工作日计算 ===\n",
      "时间范围: 2024-01-01 至 2024-01-31\n",
      "总天数: 31天\n",
      "工作日: 23天\n",
      "周末: 8天\n"
     ]
    }
   ],
   "source": [
    "# 计算工作日\n",
    "print(\"\\n=== 工作日计算 ===\")\n",
    "\n",
    "start = pd.Timestamp('2024-01-01')\n",
    "end = pd.Timestamp('2024-01-31')\n",
    "\n",
    "# 生成日期范围\n",
    "date_range = pd.date_range(start, end, freq='D')\n",
    "\n",
    "# 计算工作日（周一到周五）\n",
    "workdays = date_range[date_range.weekday < 5]\n",
    "\n",
    "print(f\"时间范围: {start.date()} 至 {end.date()}\")\n",
    "print(f\"总天数: {len(date_range)}天\")\n",
    "print(f\"工作日: {len(workdays)}天\")\n",
    "print(f\"周末: {len(date_range) - len(workdays)}天\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.5 日期格式化输出"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== 日期格式化 ===\n",
      "\n",
      "原始: 2024-01-15 14:30:45\n",
      "\n",
      "标准格式         %Y-%m-%d                  → 2024-01-15\n",
      "斜杠分隔         %Y/%m/%d                  → 2024/01/15\n",
      "中文格式         %Y年%m月%d日                 → 2024年01月15日\n",
      "带时间          %Y-%m-%d %H:%M:%S         → 2024-01-15 14:30:45\n",
      "12小时制        %Y-%m-%d %I:%M:%S %p      → 2024-01-15 02:30:45 PM\n",
      "月份名          %d-%b-%Y                  → 15-Jan-2024\n",
      "完整月份         %B %d, %Y                 → January 15, 2024\n",
      "星期           %A, %Y-%m-%d              → Monday, 2024-01-15\n"
     ]
    }
   ],
   "source": [
    "# 日期格式化\n",
    "print(\"=== 日期格式化 ===\")\n",
    "\n",
    "sample_date = pd.Timestamp('2024-01-15 14:30:45')\n",
    "\n",
    "formats = {\n",
    "    '标准格式': '%Y-%m-%d',\n",
    "    '斜杠分隔': '%Y/%m/%d',\n",
    "    '中文格式': '%Y年%m月%d日',\n",
    "    '带时间': '%Y-%m-%d %H:%M:%S',\n",
    "    '12小时制': '%Y-%m-%d %I:%M:%S %p',\n",
    "    '月份名': '%d-%b-%Y',\n",
    "    '完整月份': '%B %d, %Y',\n",
    "    '星期': '%A, %Y-%m-%d'\n",
    "}\n",
    "\n",
    "print(f\"\\n原始: {sample_date}\\n\")\n",
    "for name, fmt in formats.items():\n",
    "    formatted = sample_date.strftime(fmt)\n",
    "    print(f\"{name:12} {fmt:25} → {formatted}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 三、字符串处理\n",
    "\n",
    "### 3.1 字符串处理的重要性\n",
    "\n",
    "**常见问题**:\n",
    "- 前后空格\n",
    "- 大小写不一致\n",
    "- 特殊字符\n",
    "- 格式不统一\n",
    "- 需要提取信息\n",
    "\n",
    "**Excel对比**:\n",
    "- Excel: TRIM(), UPPER(), LOWER(), LEFT(), RIGHT(), MID(), SUBSTITUTE()\n",
    "- Pandas: `str`方法链，功能更强大\n",
    "\n",
    "### 3.2 基础字符串操作"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== 原始字符串数据 ===\n",
      "           name                email              phone     address  \\\n",
      "0          张三      ZHANG@COMPANY.COM      138-1234-5678      北京市朝阳区   \n",
      "1         LI Si     lisi@company.com        13912345678    上海市 浦东新区   \n",
      "2            王五   wangwu@COMPANY.com      139 1234 5678    广州市天河区     \n",
      "3    ZHAO Liu    zhaoliu@company.COM  +86-139-1234-5678     深圳市南山区！   \n",
      "4       qian Qi   qianqi@Company.Com     (139)1234-5678    杭州市 西湖区？   \n",
      "\n",
      "              id_card  \n",
      "0  110101199001011234  \n",
      "1  31010119850615432X  \n",
      "2  440101199203051234  \n",
      "3  440301198807121234  \n",
      "4  330101199512251234  \n"
     ]
    }
   ],
   "source": [
    "# 创建包含各种字符串问题的数据\n",
    "messy_strings = pd.DataFrame({\n",
    "    'name': ['  张三  ', 'LI Si', '王五', '  ZHAO Liu  ', 'qian Qi'],\n",
    "    'email': ['ZHANG@COMPANY.COM', 'lisi@company.com', 'wangwu@COMPANY.com', 'zhaoliu@company.COM', 'qianqi@Company.Com'],\n",
    "    'phone': ['138-1234-5678', '13912345678', '139 1234 5678', '+86-139-1234-5678', '(139)1234-5678'],\n",
    "    'address': ['北京市朝阳区', '上海市 浦东新区', '  广州市天河区  ', '深圳市南山区！', '杭州市 西湖区？'],\n",
    "    'id_card': ['110101199001011234', '31010119850615432X', '440101199203051234', '440301198807121234', '330101199512251234']\n",
    "})\n",
    "\n",
    "print(\"=== 原始字符串数据 ===\")\n",
    "print(messy_strings)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== 1. 去除空格 ===\n",
      "去除前后空格:\n",
      "           name name_strip\n",
      "0          张三           张三\n",
      "1         LI Si      LI Si\n",
      "2            王五         王五\n",
      "3    ZHAO Liu     ZHAO Liu\n",
      "4       qian Qi    qian Qi\n",
      "\n",
      "去除所有空格:\n",
      "               phone     phone_no_space\n",
      "0      138-1234-5678      138-1234-5678\n",
      "1        13912345678        13912345678\n",
      "2      139 1234 5678        13912345678\n",
      "3  +86-139-1234-5678  +86-139-1234-5678\n",
      "4     (139)1234-5678     (139)1234-5678\n"
     ]
    }
   ],
   "source": [
    "# 1. 去除空格\n",
    "print(\"\\n=== 1. 去除空格 ===\")\n",
    "\n",
    "df_str = messy_strings.copy()\n",
    "\n",
    "df_str['name_strip'] = df_str['name'].str.strip()  # 前后空格\n",
    "df_str['address_strip'] = df_str['address'].str.strip()  # 前后空格\n",
    "\n",
    "print(\"去除前后空格:\")\n",
    "print(df_str[['name', 'name_strip']])\n",
    "\n",
    "# 去除所有空格\n",
    "df_str['phone_no_space'] = df_str['phone'].str.replace(' ', '')\n",
    "print(\"\\n去除所有空格:\")\n",
    "print(df_str[['phone', 'phone_no_space']])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== 2. 大小写转换 ===\n",
      "邮箱标准化（全小写）:\n",
      "                 email          email_lower\n",
      "0    ZHANG@COMPANY.COM    zhang@company.com\n",
      "1     lisi@company.com     lisi@company.com\n",
      "2   wangwu@COMPANY.com   wangwu@company.com\n",
      "3  zhaoliu@company.COM  zhaoliu@company.com\n",
      "4   qianqi@Company.Com   qianqi@company.com\n",
      "\n",
      "姓名标准化（首字母大写）:\n",
      "           name name_title\n",
      "0          张三           张三\n",
      "1         LI Si      Li Si\n",
      "2            王五         王五\n",
      "3    ZHAO Liu     Zhao Liu\n",
      "4       qian Qi    Qian Qi\n"
     ]
    }
   ],
   "source": [
    "# 2. 大小写转换\n",
    "print(\"\\n=== 2. 大小写转换 ===\")\n",
    "\n",
    "df_str['email_lower'] = df_str['email'].str.lower()  # 全小写\n",
    "df_str['email_upper'] = df_str['email'].str.upper()  # 全大写\n",
    "df_str['name_title'] = df_str['name'].str.strip().str.title()  # 首字母大写\n",
    "\n",
    "print(\"邮箱标准化（全小写）:\")\n",
    "print(df_str[['email', 'email_lower']].head())\n",
    "\n",
    "print(\"\\n姓名标准化（首字母大写）:\")\n",
    "print(df_str[['name', 'name_title']].head())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== 3. 替换和删除 ===\n",
      "清洗电话号码:\n",
      "               phone  phone_clean\n",
      "0      138-1234-5678  13812345678\n",
      "1        13912345678  13912345678\n",
      "2      139 1234 5678  13912345678\n",
      "3  +86-139-1234-5678  13912345678\n",
      "4     (139)1234-5678  13912345678\n",
      "\n",
      "删除标点:\n",
      "      address address_clean\n",
      "0      北京市朝阳区        北京市朝阳区\n",
      "1    上海市 浦东新区      上海市 浦东新区\n",
      "2    广州市天河区        广州市天河区  \n",
      "3     深圳市南山区！        深圳市南山区\n",
      "4    杭州市 西湖区？       杭州市 西湖区\n"
     ]
    }
   ],
   "source": [
    "# 3. 替换和删除\n",
    "print(\"\\n=== 3. 替换和删除 ===\")\n",
    "\n",
    "# 替换字符\n",
    "df_str['phone_clean'] = (df_str['phone']\n",
    "    .str.replace('-', '', regex=False)\n",
    "    .str.replace(' ', '', regex=False)\n",
    "    .str.replace('(', '', regex=False)\n",
    "    .str.replace(')', '', regex=False)\n",
    "    .str.replace('+86', '', regex=False)\n",
    ")\n",
    "\n",
    "print(\"清洗电话号码:\")\n",
    "print(df_str[['phone', 'phone_clean']])\n",
    "\n",
    "# 删除标点符号\n",
    "df_str['address_clean'] = df_str['address'].str.replace('[！？。，]', '', regex=True)\n",
    "print(\"\\n删除标点:\")\n",
    "print(df_str[['address', 'address_clean']])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3.3 字符串提取和分割"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== 字符串提取 ===\n",
      "从身份证号提取信息:\n",
      "              id_card birth_year birth_month birth_day gender\n",
      "0  110101199001011234       1990          01        01      男\n",
      "1  31010119850615432X       1985          06        15      女\n",
      "2  440101199203051234       1992          03        05      男\n",
      "3  440301198807121234       1988          07        12      男\n",
      "4  330101199512251234       1995          12        25      男\n"
     ]
    }
   ],
   "source": [
    "# 字符串提取\n",
    "print(\"=== 字符串提取 ===\")\n",
    "\n",
    "# 从身份证号提取信息\n",
    "df_str['birth_year'] = df_str['id_card'].str[6:10]\n",
    "df_str['birth_month'] = df_str['id_card'].str[10:12]\n",
    "df_str['birth_day'] = df_str['id_card'].str[12:14]\n",
    "df_str['gender_code'] = df_str['id_card'].str[16:17]\n",
    "\n",
    "# 性别判断（倒数第二位，奇数为男，偶数为女）\n",
    "df_str['gender'] = df_str['gender_code'].apply(\n",
    "    lambda x: '男' if x.isdigit() and int(x) % 2 == 1 else '女'\n",
    ")\n",
    "\n",
    "print(\"从身份证号提取信息:\")\n",
    "print(df_str[['id_card', 'birth_year', 'birth_month', 'birth_day', 'gender']])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== 字符串分割 ===\n",
      "        full_address   省市     区        街道\n",
      "0   北京市-朝阳区-建国门外大街1号  北京市   朝阳区  建国门外大街1号\n",
      "1  上海市-浦东新区-世纪大道100号  上海市  浦东新区  世纪大道100号\n",
      "2    广州市-天河区-天河路123号  广州市   天河区   天河路123号\n"
     ]
    }
   ],
   "source": [
    "# 字符串分割\n",
    "print(\"\\n=== 字符串分割 ===\")\n",
    "\n",
    "# 创建包含多部分的数据\n",
    "address_data = pd.DataFrame({\n",
    "    'full_address': [\n",
    "        '北京市-朝阳区-建国门外大街1号',\n",
    "        '上海市-浦东新区-世纪大道100号',\n",
    "        '广州市-天河区-天河路123号'\n",
    "    ]\n",
    "})\n",
    "\n",
    "# 分割成多列\n",
    "address_split = address_data['full_address'].str.split('-', expand=True)\n",
    "address_split.columns = ['省市', '区', '街道']\n",
    "\n",
    "result = pd.concat([address_data, address_split], axis=1)\n",
    "print(result)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3.4 字符串查询和匹配"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== 字符串查询 ===\n",
      "\n",
      "地址中包含'区'的:\n",
      "           name     address\n",
      "0          张三        北京市朝阳区\n",
      "1         LI Si    上海市 浦东新区\n",
      "2            王五    广州市天河区  \n",
      "3    ZHAO Liu       深圳市南山区！\n",
      "4       qian Qi    杭州市 西湖区？\n",
      "\n",
      "地址以'北京'开头的:\n",
      "     name address\n",
      "0    张三    北京市朝阳区\n",
      "\n",
      "邮箱以.com结尾的:\n",
      "    name               email\n",
      "1  LI Si    lisi@company.com\n",
      "2     王五  wangwu@COMPANY.com\n"
     ]
    }
   ],
   "source": [
    "# 字符串查询\n",
    "print(\"=== 字符串查询 ===\")\n",
    "\n",
    "# 包含\n",
    "print(\"\\n地址中包含'区'的:\")\n",
    "contains_qu = df_str[df_str['address'].str.contains('区', na=False)]\n",
    "print(contains_qu[['name', 'address']])\n",
    "\n",
    "# 以...开头\n",
    "print(\"\\n地址以'北京'开头的:\")\n",
    "starts_beijing = df_str[df_str['address'].str.startswith('北京', na=False)]\n",
    "print(starts_beijing[['name', 'address']])\n",
    "\n",
    "# 以...结尾\n",
    "print(\"\\n邮箱以.com结尾的:\")\n",
    "ends_com = df_str[df_str['email'].str.endswith('.com', na=False)]\n",
    "print(ends_com[['name', 'email']])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3.5 正则表达式 (Regex)\n",
    "\n",
    "**强大的文本模式匹配工具**\n",
    "\n",
    "**常用模式**:\n",
    "- `\\d`: 数字\n",
    "- `\\w`: 字母数字下划线\n",
    "- `\\s`: 空白字符\n",
    "- `.`: 任意字符\n",
    "- `*`: 0次或多次\n",
    "- `+`: 1次或多次\n",
    "- `?`: 0次或1次\n",
    "- `[]`: 字符集\n",
    "- `^`: 开头\n",
    "- `$`: 结尾"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== 正则表达式 ===\n",
      "原始文本:\n",
      "                   text\n",
      "0     我的手机号是13812345678\n",
      "1    联系电话: 021-12345678\n",
      "2  邮箱: user@example.com\n",
      "3   订单号: ORD20240115001\n",
      "4         价格：¥1,234.56元\n"
     ]
    }
   ],
   "source": [
    "# 正则表达式示例\n",
    "print(\"=== 正则表达式 ===\")\n",
    "\n",
    "# 创建测试数据\n",
    "text_data = pd.DataFrame({\n",
    "    'text': [\n",
    "        '我的手机号是13812345678',\n",
    "        '联系电话: 021-12345678',\n",
    "        '邮箱: user@example.com',\n",
    "        '订单号: ORD20240115001',\n",
    "        '价格：¥1,234.56元'\n",
    "    ]\n",
    "})\n",
    "\n",
    "print(\"原始文本:\")\n",
    "print(text_data)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "提取手机号:\n",
      "                   text       mobile\n",
      "0     我的手机号是13812345678  13812345678\n",
      "1    联系电话: 021-12345678          NaN\n",
      "2  邮箱: user@example.com          NaN\n",
      "3   订单号: ORD20240115001          NaN\n",
      "4         价格：¥1,234.56元          NaN\n"
     ]
    }
   ],
   "source": [
    "# 提取手机号（11位数字）\n",
    "text_data['mobile'] = text_data['text'].str.extract(r'(1[3-9]\\d{9})')\n",
    "print(\"\\n提取手机号:\")\n",
    "print(text_data[['text', 'mobile']])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "提取邮箱:\n",
      "                   text   email_extracted\n",
      "2  邮箱: user@example.com  user@example.com\n"
     ]
    }
   ],
   "source": [
    "# 提取邮箱\n",
    "text_data['email_extracted'] = text_data['text'].str.extract(r'([\\w\\.-]+@[\\w\\.-]+\\.\\w+)')\n",
    "print(\"\\n提取邮箱:\")\n",
    "print(text_data[['text', 'email_extracted']].dropna())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "提取所有数字:\n",
      "                   text       numbers\n",
      "0     我的手机号是13812345678   13812345678\n",
      "1    联系电话: 021-12345678  021,12345678\n",
      "2  邮箱: user@example.com              \n",
      "3   订单号: ORD20240115001   20240115001\n",
      "4         价格：¥1,234.56元      1,234,56\n"
     ]
    }
   ],
   "source": [
    "# 提取数字\n",
    "text_data['numbers'] = text_data['text'].str.findall(r'\\d+').str.join(',')\n",
    "print(\"\\n提取所有数字:\")\n",
    "print(text_data[['text', 'numbers']])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== 格式验证 ===\n",
      "                    email  is_valid\n",
      "0        user@example.com      True\n",
      "1           invalid.email     False\n",
      "2      another@test.co.uk      True\n",
      "3              @wrong.com     False\n",
      "4  good_email@company.com      True\n"
     ]
    }
   ],
   "source": [
    "# 验证格式\n",
    "print(\"\\n=== 格式验证 ===\")\n",
    "\n",
    "# 验证邮箱格式\n",
    "emails = pd.Series([\n",
    "    'user@example.com',\n",
    "    'invalid.email',\n",
    "    'another@test.co.uk',\n",
    "    '@wrong.com',\n",
    "    'good_email@company.com'\n",
    "])\n",
    "\n",
    "# 邮箱正则\n",
    "email_pattern = r'^[\\w\\.-]+@[\\w\\.-]+\\.\\w+$'\n",
    "emails_valid = emails.str.match(email_pattern)\n",
    "\n",
    "result_df = pd.DataFrame({\n",
    "    'email': emails,\n",
    "    'is_valid': emails_valid\n",
    "})\n",
    "print(result_df)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3.6 字符串方法大全\n",
    "\n",
    "| 方法 | 说明 | Excel等价 |\n",
    "|------|------|----------|\n",
    "| `str.strip()` | 去除前后空格 | `TRIM()` |\n",
    "| `str.lower()` | 转小写 | `LOWER()` |\n",
    "| `str.upper()` | 转大写 | `UPPER()` |\n",
    "| `str.title()` | 首字母大写 | `PROPER()` |\n",
    "| `str.replace()` | 替换 | `SUBSTITUTE()` |\n",
    "| `str.split()` | 分割 | `分列` |\n",
    "| `str.contains()` | 包含 | `SEARCH()` |\n",
    "| `str.startswith()` | 以...开头 | `LEFT()+IF()` |\n",
    "| `str.endswith()` | 以...结尾 | `RIGHT()+IF()` |\n",
    "| `str.len()` | 长度 | `LEN()` |\n",
    "| `str[:n]` | 取前n个字符 | `LEFT()` |\n",
    "| `str[-n:]` | 取后n个字符 | `RIGHT()` |\n",
    "| `str[a:b]` | 取中间字符 | `MID()` |\n",
    "| `str.extract()` | 正则提取 | 复杂公式 |\n",
    "| `str.findall()` | 查找所有匹配 | 无直接等价 |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 四、数据去重\n",
    "\n",
    "### 4.1 重复数据的危害\n",
    "\n",
    "**影响**:\n",
    "- 统计指标失真（计数、求和）\n",
    "- 存储空间浪费\n",
    "- 分析结果偏差\n",
    "- 用户体验差（重复通知）\n",
    "\n",
    "**产生原因**:\n",
    "- 数据录入重复\n",
    "- 系统bug\n",
    "- 数据合并时重复\n",
    "- ETL流程错误\n",
    "\n",
    "### 4.2 检测重复值"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== 包含重复的数据 ===\n",
      "  order_id customer product  amount       date\n",
      "0     O001       张三      手机    3999 2024-01-01\n",
      "1     O002       李四      电脑    8999 2024-01-02\n",
      "2     O003       王五      平板    2999 2024-01-03\n",
      "3     O002       李四      电脑    8999 2024-01-02\n",
      "4     O004       赵六      手机    3999 2024-01-04\n",
      "5     O001       张三      手机    3999 2024-01-01\n",
      "6     O005       钱七      耳机     299 2024-01-05\n",
      "7     O003       王五      平板    2999 2024-01-03\n",
      "\n",
      "总行数: 8\n"
     ]
    }
   ],
   "source": [
    "# 创建包含重复的数据\n",
    "duplicate_data = pd.DataFrame({\n",
    "    'order_id': ['O001', 'O002', 'O003', 'O002', 'O004', 'O001', 'O005', 'O003'],\n",
    "    'customer': ['张三', '李四', '王五', '李四', '赵六', '张三', '钱七', '王五'],\n",
    "    'product': ['手机', '电脑', '平板', '电脑', '手机', '手机', '耳机', '平板'],\n",
    "    'amount': [3999, 8999, 2999, 8999, 3999, 3999, 299, 2999],\n",
    "    'date': pd.to_datetime(['2024-01-01', '2024-01-02', '2024-01-03', '2024-01-02', \n",
    "                             '2024-01-04', '2024-01-01', '2024-01-05', '2024-01-03'])\n",
    "})\n",
    "\n",
    "print(\"=== 包含重复的数据 ===\")\n",
    "print(duplicate_data)\n",
    "print(f\"\\n总行数: {len(duplicate_data)}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== 检测完全重复 ===\n",
      "完全重复的行数: 3\n",
      "\n",
      "完全重复的记录:\n",
      "  order_id customer product  amount       date\n",
      "0     O001       张三      手机    3999 2024-01-01\n",
      "5     O001       张三      手机    3999 2024-01-01\n",
      "1     O002       李四      电脑    8999 2024-01-02\n",
      "3     O002       李四      电脑    8999 2024-01-02\n",
      "2     O003       王五      平板    2999 2024-01-03\n",
      "7     O003       王五      平板    2999 2024-01-03\n"
     ]
    }
   ],
   "source": [
    "# 检测完全重复的行\n",
    "print(\"\\n=== 检测完全重复 ===\")\n",
    "print(f\"完全重复的行数: {duplicate_data.duplicated().sum()}\")\n",
    "\n",
    "# 查看重复的行\n",
    "print(\"\\n完全重复的记录:\")\n",
    "fully_duplicated = duplicate_data[duplicate_data.duplicated(keep=False)]\n",
    "print(fully_duplicated.sort_values('order_id'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== 基于order_id检测重复 ===\n",
      "order_id重复数: 6\n",
      "\n",
      "order_id重复的记录:\n",
      "  order_id customer product  amount       date\n",
      "0     O001       张三      手机    3999 2024-01-01\n",
      "5     O001       张三      手机    3999 2024-01-01\n",
      "1     O002       李四      电脑    8999 2024-01-02\n",
      "3     O002       李四      电脑    8999 2024-01-02\n",
      "2     O003       王五      平板    2999 2024-01-03\n",
      "7     O003       王五      平板    2999 2024-01-03\n"
     ]
    }
   ],
   "source": [
    "# 基于特定列检测重复\n",
    "print(\"\\n=== 基于order_id检测重复 ===\")\n",
    "order_duplicated = duplicate_data.duplicated(subset=['order_id'], keep=False)\n",
    "print(f\"order_id重复数: {order_duplicated.sum()}\")\n",
    "\n",
    "print(\"\\norder_id重复的记录:\")\n",
    "print(duplicate_data[order_duplicated].sort_values('order_id'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== 重复情况分析 ===\n",
      "\n",
      "order_id出现次数:\n",
      "order_id\n",
      "O001    2\n",
      "O002    2\n",
      "O003    2\n",
      "O004    1\n",
      "O005    1\n",
      "Name: count, dtype: int64\n",
      "\n",
      "重复的order_id: ['O001', 'O002', 'O003']\n",
      "重复的订单数: 3\n"
     ]
    }
   ],
   "source": [
    "# 分析重复情况\n",
    "print(\"\\n=== 重复情况分析 ===\")\n",
    "\n",
    "# 统计每个order_id的出现次数\n",
    "order_counts = duplicate_data['order_id'].value_counts()\n",
    "print(\"\\norder_id出现次数:\")\n",
    "print(order_counts)\n",
    "\n",
    "# 重复的order_id\n",
    "duplicated_orders = order_counts[order_counts > 1]\n",
    "print(f\"\\n重复的order_id: {duplicated_orders.index.tolist()}\")\n",
    "print(f\"重复的订单数: {len(duplicated_orders)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.3 去重策略\n",
    "\n",
    "**keep参数**:\n",
    "- `'first'`: 保留第一次出现的（默认）\n",
    "- `'last'`: 保留最后一次出现的\n",
    "- `False`: 删除所有重复（包括第一次）\n",
    "\n",
    "**Excel对比**:\n",
    "- Excel: 数据→删除重复项，只能保留第一条\n",
    "- Pandas: 灵活控制保留哪一条"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== 策略1: 删除完全重复 ===\n",
      "原始: 8行 → 去重后: 5行\n",
      "删除了3行\n",
      "\n",
      "去重后数据:\n",
      "  order_id customer product  amount       date\n",
      "0     O001       张三      手机    3999 2024-01-01\n",
      "1     O002       李四      电脑    8999 2024-01-02\n",
      "2     O003       王五      平板    2999 2024-01-03\n",
      "4     O004       赵六      手机    3999 2024-01-04\n",
      "6     O005       钱七      耳机     299 2024-01-05\n"
     ]
    }
   ],
   "source": [
    "# 策略1: 删除完全重复的行\n",
    "print(\"=== 策略1: 删除完全重复 ===\")\n",
    "df_dedup1 = duplicate_data.drop_duplicates()\n",
    "print(f\"原始: {len(duplicate_data)}行 → 去重后: {len(df_dedup1)}行\")\n",
    "print(f\"删除了{len(duplicate_data) - len(df_dedup1)}行\")\n",
    "print(\"\\n去重后数据:\")\n",
    "print(df_dedup1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== 策略2: 基于order_id去重(保留第一条) ===\n",
      "原始: 8行 → 去重后: 5行\n",
      "\n",
      "去重后数据:\n",
      "  order_id customer product  amount       date\n",
      "0     O001       张三      手机    3999 2024-01-01\n",
      "1     O002       李四      电脑    8999 2024-01-02\n",
      "2     O003       王五      平板    2999 2024-01-03\n",
      "4     O004       赵六      手机    3999 2024-01-04\n",
      "6     O005       钱七      耳机     299 2024-01-05\n"
     ]
    }
   ],
   "source": [
    "# 策略2: 基于order_id去重，保留第一条\n",
    "print(\"\\n=== 策略2: 基于order_id去重(保留第一条) ===\")\n",
    "df_dedup2 = duplicate_data.drop_duplicates(subset=['order_id'], keep='first')\n",
    "print(f\"原始: {len(duplicate_data)}行 → 去重后: {len(df_dedup2)}行\")\n",
    "print(\"\\n去重后数据:\")\n",
    "print(df_dedup2.sort_values('order_id'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== 策略3: 基于order_id去重(保留最后一条) ===\n",
      "原始: 8行 → 去重后: 5行\n",
      "\n",
      "去重后数据:\n",
      "  order_id customer product  amount       date\n",
      "5     O001       张三      手机    3999 2024-01-01\n",
      "3     O002       李四      电脑    8999 2024-01-02\n",
      "7     O003       王五      平板    2999 2024-01-03\n",
      "4     O004       赵六      手机    3999 2024-01-04\n",
      "6     O005       钱七      耳机     299 2024-01-05\n"
     ]
    }
   ],
   "source": [
    "# 策略3: 保留最后一条\n",
    "print(\"\\n=== 策略3: 基于order_id去重(保留最后一条) ===\")\n",
    "df_dedup3 = duplicate_data.drop_duplicates(subset=['order_id'], keep='last')\n",
    "print(f\"原始: {len(duplicate_data)}行 → 去重后: {len(df_dedup3)}行\")\n",
    "print(\"\\n去重后数据:\")\n",
    "print(df_dedup3.sort_values('order_id'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== 策略4: 删除所有重复 ===\n",
      "原始: 8行 → 去重后: 2行\n",
      "\n",
      "只保留不重复的记录:\n",
      "  order_id customer product  amount       date\n",
      "4     O004       赵六      手机    3999 2024-01-04\n",
      "6     O005       钱七      耳机     299 2024-01-05\n"
     ]
    }
   ],
   "source": [
    "# 策略4: 删除所有重复（包括第一次）\n",
    "print(\"\\n=== 策略4: 删除所有重复 ===\")\n",
    "df_dedup4 = duplicate_data.drop_duplicates(subset=['order_id'], keep=False)\n",
    "print(f\"原始: {len(duplicate_data)}行 → 去重后: {len(df_dedup4)}行\")\n",
    "print(\"\\n只保留不重复的记录:\")\n",
    "print(df_dedup4.sort_values('order_id'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== 策略5: 基于客户+产品组合去重 ===\n",
      "原始: 8行 → 去重后: 5行\n",
      "\n",
      "去重后数据:\n",
      "  order_id customer product  amount       date\n",
      "0     O001       张三      手机    3999 2024-01-01\n",
      "1     O002       李四      电脑    8999 2024-01-02\n",
      "2     O003       王五      平板    2999 2024-01-03\n",
      "4     O004       赵六      手机    3999 2024-01-04\n",
      "6     O005       钱七      耳机     299 2024-01-05\n"
     ]
    }
   ],
   "source": [
    "# 策略5: 根据多列组合去重\n",
    "print(\"\\n=== 策略5: 基于客户+产品组合去重 ===\")\n",
    "df_dedup5 = duplicate_data.drop_duplicates(subset=['customer', 'product'], keep='first')\n",
    "print(f\"原始: {len(duplicate_data)}行 → 去重后: {len(df_dedup5)}行\")\n",
    "print(\"\\n去重后数据:\")\n",
    "print(df_dedup5.sort_values(['customer', 'product']))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.4 智能去重 - 保留最优记录\n",
    "\n",
    "**场景**: 重复记录中，希望保留最新的、最完整的、金额最大的等"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== 智能去重: 保留最新记录 ===\n",
      "\n",
      "原始数据（按order_id排序）:\n",
      "  order_id       date  amount\n",
      "0     O001 2024-01-01    3999\n",
      "5     O001 2024-01-01    3999\n",
      "1     O002 2024-01-02    8999\n",
      "3     O002 2024-01-02    8999\n",
      "2     O003 2024-01-03    2999\n",
      "7     O003 2024-01-03    2999\n",
      "4     O004 2024-01-04    3999\n",
      "6     O005 2024-01-05     299\n",
      "\n",
      "保留最新记录:\n",
      "  order_id       date  amount\n",
      "0     O001 2024-01-01    3999\n",
      "1     O002 2024-01-02    8999\n",
      "2     O003 2024-01-03    2999\n",
      "4     O004 2024-01-04    3999\n",
      "6     O005 2024-01-05     299\n"
     ]
    }
   ],
   "source": [
    "# 智能去重：保留日期最新的\n",
    "print(\"=== 智能去重: 保留最新记录 ===\")\n",
    "\n",
    "# 先按date降序排序，再去重（会保留最新的）\n",
    "df_dedup_latest = (duplicate_data\n",
    "    .sort_values('date', ascending=False)\n",
    "    .drop_duplicates(subset=['order_id'], keep='first')\n",
    "    .sort_values('order_id')\n",
    ")\n",
    "\n",
    "print(\"\\n原始数据（按order_id排序）:\")\n",
    "print(duplicate_data.sort_values('order_id')[['order_id', 'date', 'amount']])\n",
    "\n",
    "print(\"\\n保留最新记录:\")\n",
    "print(df_dedup_latest[['order_id', 'date', 'amount']])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== 智能去重: 保留金额最大的记录 ===\n",
      "\n",
      "每个客户保留最大金额的订单:\n",
      "  customer order_id  amount\n",
      "0       张三     O001    3999\n",
      "1       李四     O002    8999\n",
      "2       王五     O003    2999\n",
      "4       赵六     O004    3999\n",
      "6       钱七     O005     299\n"
     ]
    }
   ],
   "source": [
    "# 智能去重：保留金额最大的\n",
    "print(\"\\n=== 智能去重: 保留金额最大的记录 ===\")\n",
    "\n",
    "# 先按amount降序排序，再去重\n",
    "df_dedup_max = (duplicate_data\n",
    "    .sort_values('amount', ascending=False)\n",
    "    .drop_duplicates(subset=['customer'], keep='first')\n",
    "    .sort_values('customer')\n",
    ")\n",
    "\n",
    "print(\"\\n每个客户保留最大金额的订单:\")\n",
    "print(df_dedup_max[['customer', 'order_id', 'amount']])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.5 去重前后对比"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAABW0AAAHqCAYAAAB/bWzAAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAm6RJREFUeJzs3Xl4E9X+x/FPQmm6s7asZRUEQbxSK6sCooACyqLsm6LIJl5QhMqVRdEq130pClxZFC+ICIIKooIiiAL1h2URZV+K0BaEtkDLkvP7A5tL6EICbZLS9+t55oFMTuZ8MyczPflm5hyLMcYIAAAAAAAAAOATrN4OAAAAAAAAAADwPyRtAQAAAAAAAMCHkLQFAAAAAAAAAB9C0hYAAAAAAAAAfAhJWwAAAAAAAADwISRtAQAAAAAAAMCHkLQFAAAAAAAAAB9C0hYAAAAAAAAAfAhJWwAAAAAAAADwISRtATdMnDhRFovFaSlWrJhCQ0NVr149jRw5Un/++WeB1v3dd98VyPazzJo1SxaLRbNmzZIk7d27VxaLRQMGDCjQeiVp69atTo+rVaumatWqFXi9eVmwYIFat26t0qVLq3jx4oqIiNA999yjxYsX51j+5MmT2rdvX4HEcuDAAaWnpxfItrN4sr1zk5GRoTfffFO33XabSpcuraCgINWtW1dDhgzRzp07vRaXN7Rs2VIWi8XbYQAA4BO82RfPicViUcuWLR2PPfV3++jRozpy5EiBbPv333/X4MGDVatWLQUFBSksLEwNGzbUpEmTdPz48Xyp49LvGwUpq2976WKz2VS5cmV16tRJK1euzPcYt23b5lK5S7/jfffdd7JYLJo4ceIV1ZuXs2fPaseOHY7HBVkXgPzh5+0AgMJo0KBBuu222yRJxhidPHlSP/30k9555x199NFHWrlyperVq+flKPNHeHi4PvjgA9WsWbNA63n00Ue1YsUK7dmzx7Hu9ddfL9A6L+fxxx/Xm2++qZYtWyomJkalSpXSwYMHNWfOHHXu3FlDhw7VO++84yi/ceNGderUSZMnT873pOf777+v4cOHa9u2bQoJCcnXbV/MU+2dmx07dqhjx47asWOHunTpogceeEA2m02bN2/WzJkz9f777+uTTz5Rx44dvRKfp40bN04PP/ywt8MAAMCn+Gpf3BN/t7/88kv16dNHn376qcqVK5ev23777bc1cuRIhYaGqnfv3rrxxht19uxZrVmzRpMnT9bUqVP12WefqVGjRvlaryfUqVNH48aNczw+c+aM9u7dq1mzZunOO+/USy+9pNGjRzuev/322/XBBx+oadOmbtfVtm1bZWZmunSxTZcuXXTdddepbt26btfjjr1796pdu3bq0aOHI0lbt25dffDBB2rQoEGB1g3gypG0Ba5AkyZN1KdPH6d1gwcP1sMPP6w777xTXbt2VUJCgvz9/b0UYf4JDg7O9l4LwldffZVtXadOnQq83tz89NNPevPNNzVkyBDFxcU5PTd27Fg1bdpUcXFx6tGjh+NLw5YtW5SYmFgg8axevVqnT58ukG1fzFPtnZNjx46pbdu2Onr0qFavXq1mzZo5Pf/444+rWbNm6tq1q7Zt26brrrvOK3F60l133eXtEAAA8Dm+2hf3xN/t9evX66+//sr37X700Ud67LHH1Lx5cy1evFhlypRxPDds2DCNHj1a7du3V9u2bbVx48ZC1w8rV65cjn3c0aNHq02bNhozZoyio6MdV07XqFFDNWrUuKK6VqxYoRYtWrhUtkGDBh5Jmu7du1e///6707rc9gkA38HwCEA+at68uUaNGqXff/9dc+fO9XY4uAqrV6+WJLVr1y7bcwEBAXrsscckSevWrfNoXNeyF198UXv27NGUKVOyJWwlqVatWnrxxRd19uxZpyucAQAAJPriV+rkyZMaMWKEwsPDsyVsszRs2FAfffSRTpw4oX/+85+eD7KAhIaGaubMmbJYLJowYYK3wwEAJyRtgXzWr18/SdInn3zitH7fvn0aMGCAKlSoIJvNppo1a2rMmDFKTU3Nto358+crKipKgYGBqlChgp566imdPXvWqUxeY48OGDBAFotFe/fudSr773//Wy+99JKqVq0qm82mG264QW+//Xae7ye3euLj49W5c2eVLVtWISEhuvnmm/Xee+/JGOMoc/78ecXFxalRo0YqW7asAgICVKVKFQ0aNEgpKSlO29+3b5/27dvnNK5STmPa7t+/X4888ogqV64sf39/RUZG6tFHH812hWvLli1Vv359bdmyRe3bt1dYWJhCQkLUpk0brV+/Ps/3LMkxBMH06dN18uTJbM/37dtXZ86c0VNPPSXpwj5/8MEHJUkPPvigYzyzrPGw/vvf/6pJkyYKCgpSkyZNlJmZKUlavny57r77blWqVEk2m03lypXTvffeq//7v/9z1FWtWjXNnj1bklS9enWnsdNSUlI0YsQIR5tGRkZqyJAhOY5ztnjxYt16660KCgpSRESEhg4dqi+//NKlMYw3b96srl27Kjw8XAEBAapXr55eeOEFx/vIsmnTJnXq1EkVKlRQUFCQrrvuOo0ePVonTpy47D6fPXu2wsLC8hxaok+fPtq2bZtee+01p/VffPGFWrdurbCwMAUGBuof//iH3nrrLdntdkeZrHG7/vvf/2r8+PGqUqWKAgICVL9+fS1cuFBnz57VxIkTVbVqVQUHB+vmm2/WsmXLnOqpVq2aOnTooKVLl+of//iHAgICVLlyZY0cOTLbeMOufP4vjuudd97R3XffrcDAQN144436888/cxwbb/r06br55psVGhqq0qVLq0mTJvroo4+y7autW7eqZ8+eKleunPz9/VWzZk09+eST2a7OyXpPP/zwg1q0aKHg4GCVLFlSnTt31h9//JFrWwAA4Ity6ovnNtZsTmN6ZvUh161bp6ZNmyowMFARERF66KGHLjuObE71pKWlaezYsapVq5YCAgJUtWpVDR06NNvYu+vWrdP999/v6NOVLVtWrVu3dhpvtWXLlpo0aZIkqVWrVk795JMnT+pf//qXateuLZvNpvLly6tPnz4uzQfw6aef6ujRoxo4cGCOCdssrVq1UuPGjfXFF19o//79ki7f1/3222912223KSgoSGXKlNEjjzyitLS0HLf/ww8/qF27dipVqpSCgoIUFRWlqVOn5tify6nfdKXq1Kmj6OhorV69WklJSU7v6+IxbS/Xz82KTZK+//57p9dbLBYNGjRII0eOVEhIiCIjI7VixYpc5y05f/68xo0bp4oVKyowMFANGzbUvHnznMrkNefJxeMtT5w4Ua1atZIkTZo0yfE9MbcxbV3pV2d9Z3j55Zc1ffp01a9fXzabTZUqVdLw4cNzbWMA7mF4BCCf1apVS1ar1Snptnv3bjVu3FinT5/WoEGDdP3112vbtm2Ki4vTl19+qbVr1yosLEySFBcXp2HDhqlBgwZ67rnnVKxYMcXFxeXLZANxcXE6ceKERo4cqRIlSjhug0pMTFRsbKzL2/nmm2909913q0yZMho6dKgqVqyozz77TIMHD9auXbs0ZcoUSdJDDz2kOXPmqFu3bnrooYdksVi0fPlyTZ8+Xb/99pt++OEHxxiqI0eOlCS99tprud4itGXLFt1+++06efKkBg0apHr16mnz5s2aPn26Fi1apNWrV6tOnTqO8keOHFGLFi3UsWNHvfLKK9q+fbvefvtttW7dWr///rsqVqyY63vs2rWrxo0bp88//1yVKlVS+/btdccdd+j2229XrVq15OfnfPp89NFHZbPZNG3aNKdx1rIMGTJE7dq1U//+/XXixAnZbDbNnDlTAwcOVHR0tEaPHq0SJUro119/1dSpU/X9999r7969KlWqlF5//XW9+uqr+uGHH/Taa685xmj766+/1KRJE+3bt08PPfSQGjZsqL1792r69OlasmSJfv75Z1WuXFmSNGfOHPXv31+1a9fWxIkTde7cOU2bNk0LFy68bHv//PPPat26tQIDAzV48GBFRkZq/fr1mjRpklauXKnly5fLz89Pu3fvVosWLVSpUiU98cQTKlWqlDZu3KhXXnlFGzZsyHNcr927dyspKUl33HGHbDZbruX8/f2zjfn1wgsvaNy4capVq5bGjBmjkJAQLVq0SCNGjNDKlSu1cOFCWa3/+41y3Lhx8vPz04gRIyRJL730krp376677rpLe/bs0T//+U+dP39eL730krp06aLt27eratWqjtcnJCTogQce0MCBAzVkyBCtWrVKr7/+ujZu3KjvvvtOxYoVk+Ta5/9i48aN06233qrXXntN+/fvV4UKFbK9/5dfflmjR49W9+7dNWTIEJ05c0YffPCBevfurTNnzjgS3t9++606dOig4sWLa8iQIapWrZrWrVunV199VZ999pnWrl2riIgIx3Y3b96s9u3bq3///urTp4/Wr1+v//znP9q4caP++OMPBQYG5tomAAD4kpz64u46cuSI2rZtq44dO+rBBx/Uxo0bNX36dK1du1YbN25UaGioS9tJT09X06ZNtXXrVvXr10+NGzfW7t279dZbb+nbb7/Vzz//rJIlS2rFihXq0KGDatasqWHDhik8PFw7d+7U22+/rTZt2mjr1q26/vrrNW7cOJUuXVqLFi3S008/rejoaEkXxme966679NNPP6lnz54aNWqUkpOTNWPGDDVs2FBr1qzJ8xb8NWvWSJLThQG5ueOOO/TTTz9pzZo16tWrl2N9Tn3dpUuXqnPnzqpSpYrGjRunkiVL6v3339f8+fOzbXfx4sW6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GjBggDIzM53eQ9Z2s/qbWVzpN7kj69b/rDumLnU1/dwsNWvWzNY3y03fvn0VGRnpeBwaGqpHH31Up06d0ooVK1zahruupF/dvHlzpzvSihcvrqioKJ0/f15Hjx4tkDiBooThEYB8Zrfbdfz4ccdt6X/88Yfsdrs+//xzhYeH5/iarFuPd+/eLUk5/jGvX7/+Vcf2j3/8I9u6rLqy6r6cffv2SVK2W9QlOf3Bli7cyr5o0SKtWrVKv/32m3bs2KHDhw87OntZHURX7NmzR5IcQwNcqn79+vr222+1d+9ep07TxZ1ESY5k3aVjBOfGYrGoefPmat68uSTp1KlT+uKLL/Tss89qy5Yt+uc//6nPP//8stvJqSNns9m0du1affnll9qyZYv++OMP7du3z9EJzmv/JCcn6+jRozp69Giun6usclkzxebUZrntzyzbt2+XJE2dOlVTp07NsUzWrVVdunTRwIED9f7772vFihXy9/dX48aN1b59ez344IN5xpk1VMWlSffLyetzUblyZZUsWdJRJsvFPzZIcgx1celtdVnJ84vH75Iu7MdLh3CIiIhQqVKlnI4jdz//rnT2X331Ve3atUuTJ0/W5MmTFR4ertatW+uBBx5Qp06dZLVa89wnFotF9erV086dO5WcnOyos0yZMtm+pLh7rAAA4Asu7YtfiYoVKzoNIyRdSEbVqFFD27Ztc3k7+/btU61atRx9iiw2m83pQgo/Pz/t27dPb7zxhhISEvTHH39o9+7djh9YL9dn3r59u06fPp0t5ovt3bs31+ez+kCHDh1SrVq18qwr68fsS/tNl/Zj3Plek9XfjImJUUxMTK7x51Xf1cpKMF763SHL1fRzryTmnIazyOrLX9q3zS9X0q/OaX/RhwTyD0lbIJ/93//9n86dO+foiGUlfO6//349+uijOb4mK4mT9W9OvxrnNc7npXLr2OW0jazO4KVjtLq77UulpqY6xvC69dZb1bhxY/Xu3VtNmjTRSy+9lOPESa7IaRIJ6X/v49J9l9uv5ZczadIklSxZUo8//rjT+qCgID3wwANq27atqlSp4hgD7HJy2r/9+vXTBx98oJo1a6pFixZq06aNbrnlFm3evFnDhw/Pc3tZn6vmzZvnOdPtxVeU5tT+AQEBLtUzbNgwderUKccyQUFBki60zYwZMzRu3Dh99tln+v777/XDDz9o9erVevHFF7VmzRrdcMMNOW6jcuXKqlKlin755RdlZmbm+nnPzMxUq1at1LRpU7388suO9Xl9Li79TOT2Wc9tG5fKLbbz5887tn0ln39XjsFatWppy5Yt+v777/XVV1/phx9+0MKFCzVv3jy1a9fOaeI0d46VKz1OAADwNZf2xfPiTp9Zcv5b7wpX+83jx4/Xc889p4oVK+qOO+7QQw89pJtvvlmnT5/Otf91Mbvdrlq1aikuLi7XMrVr1871uRYtWmjWrFn69ttv1aJFizzrWrVqlSRlm7/h0v3izvearP7mc88957jT51KlS5fOs76rtWHDBlmt1hwvcpGurp97JTFfmuiXcv++cyl3LozJiTv9avqQQMEiaQvksw8//FCS9MADD0j63y3WGRkZuvPOO7OVX7JkicqWLSvpf52pLVu26O6773Yqt2PHDqfHWcm206dPZ9vmoUOHcozt0m1I//tlO+sW8cupWbOmJDmu3rzYihUr9MEHH+jJJ5/UsmXLtHXrVj377LN65plnnMrt2rXLpbouljU77tatW3N8PiEhQRaLxWkW3asxc+ZMpaena8iQITl2msLCwlSlSpVc9/Xl/PTTT/rggw901113admyZU4dni+//PKyrw8PD1doaKiOHz+e4+fqm2++kdVqlb+/v+OKiW3btqlt27ZO5bLaPzdZ7W2xWLLVY4zRxx9/7Pj8Hjx4UDt27FCrVq30z3/+U//85z9lt9s1ZcoUxcTEaMaMGXr11Vdzratfv36aPHmy5syZo0ceeSTHMgsXLtS6descV8tWr15d0oXPxaWd7D179igtLc1xDOaXnIbE+PPPP5Wamuo4juLi4vL18y9d6Chv2bJFfn5+atWqlWOYkKSkJHXu3FnLly9XQkKC0z65lN1u15YtW1SyZMlsX34AALgWXNoXl5z7zRdPrplbP+7AgQPKyMhw+nH7zJkz2rNnj8t9ZulCP2r37t06e/asU7Lr7Nmz6tWrl+644w516tRJkydPVr169bRx40anOt99912X6zl06JBatWqVLYn2448/Kj09Pc8f6h944AE99dRTevfddzVixAhH3+5SP/zwg9auXas2bdpcts998feaS136nSSrvxkYGJitv3nq1CktXbrUaaiA/LZp0yZt3bpVbdu2dbrg4WJX2891V06ThP3222+S/rdvc/s+eKXfT7zRrwaQN8a0BfLRypUrNXXqVNWvX19du3aVdOG26dtvv11ffvmlfvrpJ6fyq1at0n333ef4Vbxt27YqU6aMpk6dqtTUVEe5v/76SzNmzHB6bZkyZRQQEKAtW7Y4jSm1c+dOrV27Nsf4FixYoP379zseZ2Zm6pVXXpHNZnPEezkNGzZU9erVNXfuXB05csTpuddff13z589XpUqVHM9dd911TmW+/fZbbdiwQZLzr8B+fn55/ipcrlw5NWvWTN98842+//57p+eyfuVu0aKF01hgV6N37946evSoRo4cmeM4q6tWrdLWrVud9lvWr+eu/LqdtX+qV6/u1Lk+fPiwY3bcS/fPxeuKFSumzp07a8uWLdnGltq+fbs6duyo8ePHy2KxqG3btipVqpTee+89paWlOcqlpaVd9stAw4YNVaNGDc2aNSvbEBqzZ89Wjx499Mknn0iSYmNjdccddzjaV7owJlyjRo0cMefliSeeUPny5fXkk0/m+BnesWOHRowYIZvN5ri6+L777pPVatWLL76oU6dOOZWfPHmyJGUbq/Vq7du3z/Ges2SN85U1i7K7n39X2O12tWzZUr1793Z6bUREhKOeYsWKKSoqSlWqVNGHH36oP/74w2kbc+fO1d69e9WpUyeXrywGAKCwyKkvLv1vaKSEhATHOrvdrjlz5uS4nTNnzjjGis/y1ltvKS0tzfG33hVdu3bViRMn9J///Mdp/aeffqpPPvlEZ8+eVVJSkowxqlSpklNi9eTJk3rrrbck5d0nlC7c0Xf8+HG98sorTvUcOXJEXbp00aBBg/K8OjM4OFhxcXFKTk5Wp06dchyLdPPmzerRo4dCQ0P1xhtvXPa9/+Mf/1DdunU1d+5cHThwwLE+MzNTb775plPZNm3aKCwsTK+99lq2Mf+nTJmiHj166IcffrhsnVfixIkTjosF8rp7zZ1+brFixa76atdZs2YpJSXF8fj48eOKi4tT6dKl1bp1a0k5f66zXnspV76neKNfDSBvXGkLXIF169Y5/vCdP39ex48f1/r16zV//nyVK1dOCxcudLr95Z133tFtt92mVq1a6ZFHHlH9+vW1Y8cOvfvuu6pcubKef/55SRduM3/33XfVvXt3RUdHa/DgwTp79qzee++9bLfTFC9eXF26dNFHH32k9u3bq1OnTtq3b5+mTp2qG264QZs2bcoWt91uV6NGjTR06FCFhobqgw8+0C+//KK4uDjHmKKXY7VaNXXqVHXs2FG33HKLHnnkEYWHh+vzzz/XsmXLNGXKFJUtW1YdOnTQG2+8occff9wxscKGDRs0d+5c1a1bV1u3btWJEycc2y1fvrzWrVun119/XY0bN87x1qi4uDi1bNlSbdu21SOPPKKbbrpJu3bt0jvvvKMyZcronXfecek9uOJf//qX1q1bp7i4OH3xxRfq0aOHatSooVOnTumnn37SwoULddNNN+nFF190eg+S9N///leSNGDAgFy336xZM5UuXVrvv/++goKCHOOM/uc//1GFChV05MiRbPtHujCmaZs2bdSpUye9+OKLWrVqlXr27Knly5erUaNGOnTokKZOnSp/f39Hhzg4OFhvv/22+vTpo6ioKD388MOSpPfee88xhmxuCbxixYpp2rRpat++vRo2bKjBgwerZs2a+uWXX/Sf//xHN954o5544glJ0ogRIxyfx0GDBql69eo6cOCApk6dqhIlSmjQoEF57vOSJUvq888/V9u2bdWiRQvdf//9uv3222WM0a+//qo5c+bIGKMPPvjAMRba9ddfr4kTJ2r8+PG6+eab1b9/f5UtW1ZffvmlPvvsM3Xo0EF9+vTJs153FS9eXH369NGaNWt0/fXX6+uvv9aiRYvUvXt3xxdEdz//rtY7duxYjR07Vi1atFC3bt0UGBiotWvXas6cObrnnnscY5DNmDFDHTt2VHR0tIYMGaJatWpp06ZNmjZtmmrUqOFIMgMAUBi52xfv0aOH3n//fd1///16/PHH5e/vrw8//DDPMTfHjx+v3377Tbfccos2bNigWbNmqXnz5hoxYoTLcY4ePVoLFizQsGHD9MsvvygqKko7d+5UXFycoqOjNXDgQPn7+6tmzZpasWKFBg4cqCZNmujgwYOaOXOm46rPnPqE06ZN0+HDh9WnTx899dRTWrJkicaMGaP169frjjvuUGpqqt59910dO3ZMn3322WV/PL///vv17rvvavjw4apdu7b69OmjG2+8UWfOnNGPP/6ojz/+WCVLltSXX37p0mRaWcMJ3HnnnWrcuLGGDx+uwMBAzZ49O9vEaiVKlNDbb7+tAQMGqH79+nrkkUdUsWJFrV69WnPnzlXr1q2velLmI0eOOK7Cli5cvLBv3z7NmTNHhw8fdnwHyY07/dzy5csrISFB7777rpo3b35Fc5PY7XbdeuutGjZsmM6fP68ZM2bo8OHDmj9/voKDgyVJ7dq1U8mSJTVx4kQdP35c1apV07fffqvVq1fnOubw559/rmrVquV4wY43+tUALsMAcNmECROMJKfFarWaUqVKmVtvvdU8++yz5vjx4zm+dufOnaZ///6mQoUKxt/f31StWtUMGjTIHDhwIFvZFStWmKZNm5qAgABTtmxZ8/jjj5sZM2YYSWbVqlWOcidPnjSjRo0ylStXNjabzTRo0MDMnTvXvPbaa0aS2bNnjzHGmD179hhJpm/fvuaZZ54x5cuXNzabzURHR5vFixc71T1z5kwjycycOdPptf3793cqt3btWtO2bVtTokQJExISYho1amTmz5/vVGb+/PmmYcOGJigoyJQqVcp06tTJrFu3znz++edGknn55ZcdZb/55htTs2ZN4+/vbwYNGmSMMaZq1aqmatWqTtvcv3+/GTRokKlUqZLx8/MzlStXNkOGDDGJiYlO5Vq0aGFyOsWtWrXKSDITJkzI9tylzp07Z2bMmGHuuusuExERYfz8/ExQUJBp2LCheemll8zp06edyp89e9b07t3bBAcHm5CQELNz585s+/Ni8fHxpm3btqZUqVImKCjINGvWzHz44Yfm2LFjxs/Pz3To0MFR9sCBA6Zx48bGZrOZunXrOtYfPnzYDB8+3FStWtX4+/ubSpUqmZ49e5pt27Zlq2/p0qXm1ltvdXyuhg0bZl566SUjyfz3v/81xuTe3r/88ovp2rWrKVu2rLHZbOa6664zo0ePNkePHnUql5CQYHr06GEiIyONzWYzVapUMf369TM7d+687P7OkpKSYsaNG2f+8Y9/mBIlSpjixYubqlWrmoEDB5rt27fn+JovvvjCtG7d2oSGhhqbzWYaNmxopk6das6fP+8ok1vbu7M+6zM5f/58c/311xt/f39z3XXXmRdffNGcO3fO6fWufv7z+kzm9DmePXu2adKkiSldurQJDg42N954o/n3v/9tMjIynMpt3brV9OzZ00RERJjixYubGjVqmDFjxmQ7R+V0nBmT/VwAAIC3XU1ffMGCBSYqKsoEBASY8uXLmxEjRpj9+/dn+xuc9bd35cqV5uabbzb+/v4mMjLSjBkzxpw8edJpm5JMixYtsr32YkePHjUjRowwkZGRxt/f3/H3+K+/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JVatWuXtEAAAAIAr4taVtlOmTNHnn3+up556SlarVWXKlNHixYs1d+7cgooPAAAAuCJffPGFHnroIbVr104DBgzQ0qVLvR0SAAAA4BK3kraHDx9W/fr1Jckxlm2VKlWUnp6e/5EBAAAAV2jgwIHq1KmTtm3bphIlSuj3339Xly5dNGDAAG+HBgAAAFyWW0nb66+/XgsWLHBat2LFCtWqVStfgwIAAACu1Pvvv69vvvlG69ev108//aT58+dr3bp1Wr9+vVatWqX333/f2yECAAAAeXIraTt58mT17dtX3bp1U0ZGhoYMGaJu3bpp0qRJBRUfAAAA4JZp06bp7bff1s033+y0/uabb9abb76p9957z0uRAQAAAK5xK2nbpk0b/fzzzwoPD1erVq1kjNE333yju+++u6DiAwAAANyybds23XnnnTk+d9ddd2nbtm0ejggAAABwj5+7L7jpppv0zjvvOK1bt26dmjRpkm9BAQAAAFfKGCNjjLfDAAAAAK6YS1fa2u12xcbGqnPnznr33Xcd640xmjRpklq0aFFgAQIAAADuuOGGG/Ttt9/m+Nw333yjunXrejgiAAAAwD0uJW3HjBmjf//73/Lz81NMTIw++ugjpaamqnXr1nrxxRf10ksvFXScAAAAgEseeeQRjRgxQgkJCU7rExISNGLECA0aNMhLkQEAAACucWl4hP/+97/6/PPP1bRpUy1btkxvvfWWZsyYoSNHjujnn39WgwYNCjpOAAAAwCUPP/ywVq9erVtuuUUNGzZUtWrVlJiYqPXr16tnz556+OGHvR0iAAAAkCeXrrQ9duyYmjZtKklq166dvv76a0kiYQsAAACfNGfOHH3yySeqW7eu/vrrL1WtWlUff/yxZs2a5e3QAAAAgMty6UpbP7//FbNYLPLz89OHH36okJCQAgsMAAAAuBr33nuv7r33Xm+HAQAAALjNpSttL2Wz2VSxYsX8jgUAAAAAAAAAirwrStparVf0MgAAAAAAAADAZbg0PEJ6erqKFSvmeGyMcXosSefPn8/fyAAAAIArkJaWptDQUG+HAQAAAFwxl5K2q1atKug4AAAAgHxRs2ZNJSUladCgQZo2bZq3wwEAAADc5lLStkWLFgUdBwAAAJAvTp8+rUWLFmnu3Lnq27evjDHZytx+++1eiAwAAABwjUtJWwAAAKCw6N+/v7p1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bX3lypUdr0tLS1OPHj1Uo0YNTZ06Ncdl9erVyszMLND9hAu/uj799NPeDgN/oz18R1Foi/79+8sYowULFuiJJ57I8W9FixYtnF4zceJEp+GLxo0bp/j4eFmtVse6Tp06uRxDcHCw0tPTlZ6erg0bNui+++7ToUOHlJmZqdOnT2vSpEn6+OOPHWVuu+22fN4LgHfs2rVLq1at0quvvqrQ0FBVqlRJY8eO1cyZM/N83W+//abBgwfrv//9ryIjIyVduFKoXr16CggI8EToPmPWrFnq1auXYmNj8yw3e/ZsNWvWTN27d1exYsXUuXNnNW/eXB9++KEkafr06Xrsscd00003qXjx4nr22Wd15MgRff/99554G17j6v5LSkrS008/rfr168tisahDhw6qW7eu1qxZI6nofv4k1/fhwYMHdfjwYTVs2DDH5/kM5r3/LsY58II9e/aoRYsWGj58uPz8/FS5cmX17dtXq1evzlaWc2DO3NmHnAcLFknbQuLDDz9UYGCg05J1IrnYzz//rEaNGmn37t3avn27/vnPf+r8+fOaNGmSPv30U+3fv18PPPCA3nrrLS+8C++744471LJlSw0YMCDXMYInTpyoe++9V1FRUZKk3bt369FHH1XHjh31/PPP64033pAk3XnnnYqPj9fSpUvVsGFDLViwwOm2vfvuu0/bt29X3759HUtCQoLefPNNp3W33HKL6tSpI0nauXOnWrVqpapVq2rXrl3auXOnUlJSnJaPP/5Yc+fO9fzOK0Dbt29Xhw4dVLZsWVWuXFnDhg3TiRMnNGvWLN1yyy3q3LmzSpQoof/+979KS0vTI488ooiICNWoUSPbF7jDhw+rR48eqlChgkqXLq2BAwfq+PHjkpTj9vLSsmVLxxiWl6v3WkJ7+A7awjuqVKmievXqKSEhQY888oheeeWVbH8rZs+eraioKEVERDi99vHHH8/1B8G89k3Lli3zHPKgbt26Klu2rPr16ydJWrNmjeLi4nIdYgcozDZv3qzSpUs7/ajdoEEDJSYmOs5bOXnyySfVvn173XHHHY51GzZskN1uV9OmTRUeHq7bb79dP/74Y0GG7xPatWunXbt2qWvXrnmW27x5sxo0aOC0rkGDBtqyZUuOzwcEBKh27dqO569Vru6/mJgYDR061PF4586d2rZtm6KjoyUV3c+f5Po+3LBhg0JD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",
      "text/plain": [
       "<Figure size 1400x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# 可视化去重效果\n",
    "fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n",
    "\n",
    "# 左图：各策略去重后的记录数\n",
    "strategies = ['原始', '完全重复', 'order_id\\n(first)', 'order_id\\n(last)', 'order_id\\n(False)', '客户+产品']\n",
    "counts = [\n",
    "    len(duplicate_data),\n",
    "    len(df_dedup1),\n",
    "    len(df_dedup2),\n",
    "    len(df_dedup3),\n",
    "    len(df_dedup4),\n",
    "    len(df_dedup5)\n",
    "]\n",
    "\n",
    "colors = ['lightblue', 'lightcoral', 'lightgreen', 'lightyellow', 'lightpink', 'lightgray']\n",
    "bars = axes[0].bar(strategies, counts, color=colors, edgecolor='black')\n",
    "axes[0].set_title('Deduplication Strategies Comparison', fontsize=14, fontweight='bold')\n",
    "axes[0].set_ylabel('Number of Records')\n",
    "axes[0].grid(axis='y', alpha=0.3)\n",
    "\n",
    "# 添加数值标签\n",
    "for bar, count in zip(bars, counts):\n",
    "    height = bar.get_height()\n",
    "    axes[0].text(bar.get_x() + bar.get_width()/2., height,\n",
    "                f'{int(count)}',\n",
    "                ha='center', va='bottom', fontweight='bold')\n",
    "\n",
    "# 右图：重复订单分布\n",
    "order_counts = duplicate_data['order_id'].value_counts().value_counts().sort_index()\n",
    "axes[1].bar(order_counts.index, order_counts.values, color='steelblue', edgecolor='black')\n",
    "axes[1].set_title('Duplicate Order Distribution', fontsize=14, fontweight='bold')\n",
    "axes[1].set_xlabel('Number of Occurrences')\n",
    "axes[1].set_ylabel('Number of Orders')\n",
    "axes[1].grid(axis='y', alpha=0.3)\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 五、数据标准化综合案例\n",
    "\n",
    "### 完整的数据清洗和标准化流程"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== 原始脏数据 ===\n",
      "  customer_id       name  gender      age           phone  \\\n",
      "0        C001       张三         男       28   138-1234-5678   \n",
      "1        c002      LI Si    Male       35     13912345678   \n",
      "2        C003         王五       M       42   139 1234 5678   \n",
      "3        C002      li si    male       35  (139)1234-5678   \n",
      "4        C004         赵六       女  unknown   138-5678-1234   \n",
      "5        c001  Zhang San       1       28     13812345678   \n",
      "6        C005         钱七  Female       31   139-8765-4321   \n",
      "\n",
      "                 email registration_date      city total_purchase  \n",
      "0    ZHANG@COMPANY.COM        2020-01-15        北京       1,234.56  \n",
      "1     lisi@company.com        2019/06/20   beijing        2345.67  \n",
      "2   wangwu@COMPANY.com       15-Mar-2021        上海          3,456  \n",
      "3     lisi@company.com        2019/06/20   Beijing        2345.67  \n",
      "4  zhaoliu@company.COM        2022.05.10      广州                0  \n",
      "5    zhang@company.com        2020-01-15       北京市        1234.56  \n",
      "6   qianqi@Company.Com          20230815  SHANGHAI         567.89  \n",
      "\n",
      "数据规模: (7, 9)\n"
     ]
    }
   ],
   "source": [
    "# 创建一个真实场景的脏数据\n",
    "messy_customer_data = pd.DataFrame({\n",
    "    'customer_id': ['C001', 'c002', 'C003', 'C002', 'C004', 'c001', 'C005'],\n",
    "    'name': ['  张三  ', 'LI Si', '王五', 'li si', '  赵六', 'Zhang San', '钱七'],\n",
    "    'gender': ['男', 'Male', 'M', 'male', '女', '1', 'Female'],\n",
    "    'age': ['28', '35', '42', '35', 'unknown', '28', '31'],\n",
    "    'phone': ['138-1234-5678', '13912345678', '139 1234 5678', '(139)1234-5678', '138-5678-1234', '13812345678', '139-8765-4321'],\n",
    "    'email': ['ZHANG@COMPANY.COM', 'lisi@company.com', 'wangwu@COMPANY.com', 'lisi@company.com', 'zhaoliu@company.COM', 'zhang@company.com', 'qianqi@Company.Com'],\n",
    "    'registration_date': ['2020-01-15', '2019/06/20', '15-Mar-2021', '2019/06/20', '2022.05.10', '2020-01-15', '20230815'],\n",
    "    'city': ['北京', 'beijing', '上海', 'Beijing', '  广州  ', '北京市', 'SHANGHAI'],\n",
    "    'total_purchase': ['1,234.56', '2345.67', '3,456', '2345.67', '0', '1234.56', '567.89']\n",
    "})\n",
    "\n",
    "print(\"=== 原始脏数据 ===\")\n",
    "print(messy_customer_data)\n",
    "print(f\"\\n数据规模: {messy_customer_data.shape}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 44,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "======================================================================\n",
      "开始数据清洗和标准化\n",
      "======================================================================\n",
      "\n",
      "步骤1: 统一customer_id格式\n",
      "完成: ['C001' 'C002' 'C003' 'C004' 'C005']\n",
      "\n",
      "步骤2: 清洗姓名\n",
      "完成: ['张三', 'LI Si', '王五', 'li si', '赵六', 'Zhang San', '钱七']\n",
      "\n",
      "步骤3: 标准化性别\n",
      "完成: {'男': 5, '女': 2}\n",
      "\n",
      "步骤4: 转换年龄类型\n",
      "完成: 类型=float64, 缺失=1\n",
      "\n",
      "步骤5: 标准化电话号码\n",
      "完成: 前3个=['13812345678', '13912345678', '13912345678']\n",
      "\n",
      "步骤6: 统一邮箱格式\n",
      "完成: 前3个=['zhang@company.com', 'lisi@company.com', 'wangwu@company.com']\n",
      "\n",
      "步骤7: 解析日期\n",
      "完成: 类型=datetime64[ns]\n",
      "\n",
      "步骤8: 标准化城市名\n",
      "完成: {'北京': 4, '上海': 2, '广州': 1}\n",
      "\n",
      "步骤9: 清洗金额\n",
      "完成: 类型=float64, 总额=¥11,184.35\n",
      "\n",
      "步骤10: 去重\n",
      "完成: 7行 → 5行 (删除2行)\n",
      "\n",
      "======================================================================\n",
      "✅ 数据清洗和标准化完成！\n",
      "======================================================================\n"
     ]
    }
   ],
   "source": [
    "# 开始清洗和标准化\n",
    "print(\"\\n\" + \"=\"*70)\n",
    "print(\"开始数据清洗和标准化\")\n",
    "print(\"=\"*70)\n",
    "\n",
    "df_clean = messy_customer_data.copy()\n",
    "\n",
    "# 步骤1: 统一customer_id大小写\n",
    "print(\"\\n步骤1: 统一customer_id格式\")\n",
    "df_clean['customer_id'] = df_clean['customer_id'].str.upper()\n",
    "print(f\"完成: {df_clean['customer_id'].unique()}\")\n",
    "\n",
    "# 步骤2: 清洗姓名（去除空格）\n",
    "print(\"\\n步骤2: 清洗姓名\")\n",
    "df_clean['name'] = df_clean['name'].str.strip()\n",
    "print(f\"完成: {df_clean['name'].tolist()}\")\n",
    "\n",
    "# 步骤3: 标准化性别\n",
    "print(\"\\n步骤3: 标准化性别\")\n",
    "gender_mapping = {\n",
    "    '男': '男', 'Male': '男', 'M': '男', 'male': '男', '1': '男',\n",
    "    '女': '女', 'Female': '女', 'F': '女', 'female': '女', '0': '女'\n",
    "}\n",
    "df_clean['gender'] = df_clean['gender'].map(gender_mapping)\n",
    "print(f\"完成: {df_clean['gender'].value_counts().to_dict()}\")\n",
    "\n",
    "# 步骤4: 转换年龄类型\n",
    "print(\"\\n步骤4: 转换年龄类型\")\n",
    "df_clean['age'] = pd.to_numeric(df_clean['age'], errors='coerce')\n",
    "print(f\"完成: 类型={df_clean['age'].dtype}, 缺失={df_clean['age'].isnull().sum()}\")\n",
    "\n",
    "# 步骤5: 标准化电话号码\n",
    "print(\"\\n步骤5: 标准化电话号码\")\n",
    "df_clean['phone'] = (df_clean['phone']\n",
    "    .str.replace('-', '', regex=False)\n",
    "    .str.replace(' ', '', regex=False)\n",
    "    .str.replace('(', '', regex=False)\n",
    "    .str.replace(')', '', regex=False)\n",
    ")\n",
    "print(f\"完成: 前3个={df_clean['phone'].head(3).tolist()}\")\n",
    "\n",
    "# 步骤6: 统一邮箱为小写\n",
    "print(\"\\n步骤6: 统一邮箱格式\")\n",
    "df_clean['email'] = df_clean['email'].str.lower()\n",
    "print(f\"完成: 前3个={df_clean['email'].head(3).tolist()}\")\n",
    "\n",
    "# 步骤7: 解析日期\n",
    "print(\"\\n步骤7: 解析日期\")\n",
    "df_clean['registration_date'] = pd.to_datetime(df_clean['registration_date'], errors='coerce')\n",
    "print(f\"完成: 类型={df_clean['registration_date'].dtype}\")\n",
    "\n",
    "# 步骤8: 标准化城市名\n",
    "print(\"\\n步骤8: 标准化城市名\")\n",
    "city_mapping = {\n",
    "    '北京': '北京', 'beijing': '北京', 'Beijing': '北京', 'BEIJING': '北京', '北京市': '北京',\n",
    "    '上海': '上海', 'shanghai': '上海', 'Shanghai': '上海', 'SHANGHAI': '上海', '上海市': '上海',\n",
    "    '广州': '广州', 'guangzhou': '广州', 'Guangzhou': '广州', '广州市': '广州'\n",
    "}\n",
    "df_clean['city'] = df_clean['city'].str.strip().map(city_mapping)\n",
    "print(f\"完成: {df_clean['city'].value_counts().to_dict()}\")\n",
    "\n",
    "# 步骤9: 清洗金额（去除逗号并转换）\n",
    "print(\"\\n步骤9: 清洗金额\")\n",
    "df_clean['total_purchase'] = df_clean['total_purchase'].str.replace(',', '').astype(float)\n",
    "print(f\"完成: 类型={df_clean['total_purchase'].dtype}, 总额=¥{df_clean['total_purchase'].sum():,.2f}\")\n",
    "\n",
    "# 步骤10: 去重（基于customer_id保留第一条）\n",
    "print(\"\\n步骤10: 去重\")\n",
    "before_dedup = len(df_clean)\n",
    "df_clean = df_clean.drop_duplicates(subset=['customer_id'], keep='first')\n",
    "after_dedup = len(df_clean)\n",
    "print(f\"完成: {before_dedup}行 → {after_dedup}行 (删除{before_dedup-after_dedup}行)\")\n",
    "\n",
    "print(\"\\n\" + \"=\"*70)\n",
    "print(\"✅ 数据清洗和标准化完成！\")\n",
    "print(\"=\"*70)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== 清洗后的数据 ===\n",
      "  customer_id   name gender   age        phone                email  \\\n",
      "0        C001     张三      男 28.00  13812345678    zhang@company.com   \n",
      "1        C002  LI Si      男 35.00  13912345678     lisi@company.com   \n",
      "2        C003     王五      男 42.00  13912345678   wangwu@company.com   \n",
      "4        C004     赵六      女   NaN  13856781234  zhaoliu@company.com   \n",
      "6        C005     钱七      女 31.00  13987654321   qianqi@company.com   \n",
      "\n",
      "  registration_date city  total_purchase  \n",
      "0        2020-01-15   北京         1234.56  \n",
      "1               NaT   北京         2345.67  \n",
      "2               NaT   上海         3456.00  \n",
      "4               NaT   广州            0.00  \n",
      "6               NaT   上海          567.89  \n",
      "\n",
      "数据类型:\n",
      "customer_id                  object\n",
      "name                         object\n",
      "gender                       object\n",
      "age                         float64\n",
      "phone                        object\n",
      "email                        object\n",
      "registration_date    datetime64[ns]\n",
      "city                         object\n",
      "total_purchase              float64\n",
      "dtype: object\n"
     ]
    }
   ],
   "source": [
    "# 查看清洗后的结果\n",
    "print(\"\\n=== 清洗后的数据 ===\")\n",
    "print(df_clean)\n",
    "\n",
    "print(\"\\n数据类型:\")\n",
    "print(df_clean.dtypes)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== 清洗前后对比 ===\n",
      "     指标    清洗前        清洗后\n",
      "    总行数      7          5\n",
      "数据类型一致性     混乱         统一\n",
      "   姓名空格    有空格        已去除\n",
      "  性别标准化   多种写法        男/女\n",
      " 电话格式统一   多种格式     纯数字11位\n",
      " 邮箱格式统一  大小写混乱        全小写\n",
      "   日期格式   多种格式 datetime64\n",
      "  城市标准化   多种写法       标准名称\n",
      "   金额类型 object    float64\n"
     ]
    }
   ],
   "source": [
    "# 清洗前后对比\n",
    "print(\"\\n=== 清洗前后对比 ===\")\n",
    "\n",
    "comparison = pd.DataFrame({\n",
    "    '指标': [\n",
    "        '总行数',\n",
    "        '数据类型一致性',\n",
    "        '姓名空格',\n",
    "        '性别标准化',\n",
    "        '电话格式统一',\n",
    "        '邮箱格式统一',\n",
    "        '日期格式',\n",
    "        '城市标准化',\n",
    "        '金额类型'\n",
    "    ],\n",
    "    '清洗前': [\n",
    "        len(messy_customer_data),\n",
    "        '混乱',\n",
    "        '有空格',\n",
    "        '多种写法',\n",
    "        '多种格式',\n",
    "        '大小写混乱',\n",
    "        '多种格式',\n",
    "        '多种写法',\n",
    "        'object'\n",
    "    ],\n",
    "    '清洗后': [\n",
    "        len(df_clean),\n",
    "        '统一',\n",
    "        '已去除',\n",
    "        '男/女',\n",
    "        '纯数字11位',\n",
    "        '全小写',\n",
    "        'datetime64',\n",
    "        '标准名称',\n",
    "        'float64'\n",
    "    ]\n",
    "})\n",
    "\n",
    "print(comparison.to_string(index=False))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "✅ 清洗后的数据已导出:\n",
      "   - cleaned_customer_data.csv\n",
      "   - cleaned_customer_data.xlsx\n"
     ]
    }
   ],
   "source": [
    "# 导出清洗后的数据\n",
    "df_clean.to_csv('cleaned_customer_data.csv', index=False, encoding='utf-8-sig')\n",
    "df_clean.to_excel('cleaned_customer_data.xlsx', index=False)\n",
    "\n",
    "print(\"\\n✅ 清洗后的数据已导出:\")\n",
    "print(\"   - cleaned_customer_data.csv\")\n",
    "print(\"   - cleaned_customer_data.xlsx\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 六、本讲总结\n",
    "\n",
    "### 核心知识点\n",
    "\n",
    "#### 1. 数据类型转换\n",
    "\n",
    "**方法**:\n",
    "- `astype()`: 基础转换\n",
    "- `to_numeric()`: 智能数字转换\n",
    "- `to_datetime()`: 日期解析\n",
    "- `category`: 优化内存\n",
    "\n",
    "**原则**:\n",
    "- 数字就是数字，不是字符串\n",
    "- 日期就是日期，方便排序和计算\n",
    "- 重复多的用category节省内存\n",
    "\n",
    "#### 2. 日期时间处理\n",
    "\n",
    "**解析**: `pd.to_datetime()` 自动识别多种格式\n",
    "\n",
    "**提取**:\n",
    "- `dt.year`, `dt.month`, `dt.day`\n",
    "- `dt.quarter`, `dt.week`\n",
    "- `dt.weekday`, `dt.day_name()`\n",
    "\n",
    "**运算**:\n",
    "- 日期差: `date1 - date2`\n",
    "- 日期偏移: `+ pd.Timedelta()`, `+ pd.DateOffset()`\n",
    "- 工作日计算\n",
    "\n",
    "#### 3. 字符串处理\n",
    "\n",
    "**基础操作**:\n",
    "- `str.strip()`: 去空格\n",
    "- `str.lower()`, `str.upper()`: 大小写\n",
    "- `str.replace()`: 替换\n",
    "- `str.split()`: 分割\n",
    "\n",
    "**查询匹配**:\n",
    "- `str.contains()`: 包含\n",
    "- `str.startswith()`: 开头\n",
    "- `str.endswith()`: 结尾\n",
    "\n",
    "**正则表达式**:\n",
    "- `str.extract()`: 提取\n",
    "- `str.findall()`: 查找所有\n",
    "- `str.match()`: 验证格式\n",
    "\n",
    "#### 4. 数据去重\n",
    "\n",
    "**检测**: `duplicated()`\n",
    "\n",
    "**去重**: `drop_duplicates()`\n",
    "- `keep='first'`: 保留第一条\n",
    "- `keep='last'`: 保留最后一条\n",
    "- `keep=False`: 删除所有重复\n",
    "\n",
    "**智能去重**:\n",
    "- 先排序再去重\n",
    "- 保留最新/最大/最完整的记录\n",
    "\n",
    "### Excel vs Pandas 对比\n",
    "\n",
    "| 任务 | Excel | Pandas |\n",
    "|------|-------|--------|\n",
    "| 类型转换 | 设置单元格格式 | `astype()` |\n",
    "| 日期解析 | DATEVALUE() | `to_datetime()` |\n",
    "| 日期提取 | YEAR(), MONTH() | `dt.year`, `dt.month` |\n",
    "| 去空格 | TRIM() | `str.strip()` |\n",
    "| 大小写 | UPPER(), LOWER() | `str.upper()`, `str.lower()` |\n",
    "| 替换 | SUBSTITUTE() | `str.replace()` |\n",
    "| 分列 | 数据→分列 | `str.split()` |\n",
    "| 去重 | 数据→删除重复项 | `drop_duplicates()` |\n",
    "| 正则 | 无 | `str.extract()`, `str.findall()` |\n",
    "\n",
    "### 数据清洗完整流程\n",
    "\n",
    "```\n",
    "1. 数据质量检查\n",
    "   ↓\n",
    "2. 类型转换\n",
    "   ↓\n",
    "3. 缺失值处理\n",
    "   ↓\n",
    "4. 异常值处理\n",
    "   ↓\n",
    "5. 格式规范化\n",
    "   - 字符串清洗\n",
    "   - 日期统一\n",
    "   - 类别标准化\n",
    "   ↓\n",
    "6. 去重\n",
    "   ↓\n",
    "7. 验证结果\n",
    "   ↓\n",
    "8. 导出清洗后数据\n",
    "```\n",
    "\n",
    "### 最佳实践\n",
    "\n",
    "1. **先检查后处理**: 了解数据问题再动手\n",
    "2. **保留原始数据**: 在副本上操作\n",
    "3. **建立映射字典**: 统一的标准化规则\n",
    "4. **批量处理**: 用循环或apply处理多列\n",
    "5. **验证结果**: 处理后检查数据类型和分布\n",
    "\n",
    "### 第三阶段完成！\n",
    "\n",
    "经过4讲学习，你已经掌握:\n",
    "- ✅ 数据分析总流程和脏数据类型\n",
    "- ✅ Pandas基础操作和数据选择\n",
    "- ✅ 缺失值和异常值的检测处理\n",
    "- ✅ 数据格式规范化和去重\n",
    "\n",
    "现在你可以处理任何脏数据了！\n",
    "\n",
    "### 下一阶段预告\n",
    "**第四阶段: 数据分析与探索(EDA)**\n",
    "- 分析思维与指标设计\n",
    "- 描述性统计分析\n",
    "- 分组与聚合分析（核心！）\n",
    "- 探索性数据分析\n",
    "- 线性回归建模\n",
    "\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 课后作业\n",
    "\n",
    "### 作业1: 类型转换实战\n",
    "给定一个数据集，完成:\n",
    "1. 识别所有类型不正确的列\n",
    "2. 将它们转换为正确的类型\n",
    "3. 处理转换过程中的错误\n",
    "4. 对比转换前后的内存占用\n",
    "5. 使用category优化内存\n",
    "\n",
    "### 作业2: 日期时间处理\n",
    "1. 解析多种格式的日期字符串\n",
    "2. 提取年、月、季度、星期等信息\n",
    "3. 计算两个日期之间的天数/月数/年数\n",
    "4. 计算某个时间范围内的工作日数量\n",
    "5. 生成未来12个月的月末日期序列\n",
    "\n",
    "### 作业3: 字符串清洗综合\n",
    "提供包含各种字符串问题的数据集:\n",
    "1. 清洗姓名（去空格、统一大小写）\n",
    "2. 标准化电话号码格式\n",
    "3. 验证邮箱格式\n",
    "4. 从地址中提取省市区\n",
    "5. 用正则表达式提取身份证号的生日和性别\n",
    "6. 清洗金额（去除货币符号和千分位）\n",
    "\n",
    "### 作业4: 去重策略对比\n",
    "1. 创建包含多种重复模式的数据\n",
    "2. 用不同策略去重并对比结果\n",
    "3. 实现智能去重（保留最优记录）\n",
    "4. 统计去重前后的数据质量变化\n",
    "5. 可视化去重效果\n",
    "\n",
    "### 作业5: 完整清洗项目\n",
    "提供一个真实的脏数据集（客户数据/订单数据），要求:\n",
    "1. 完整的数据质量报告\n",
    "2. 设计清洗方案（10个步骤）\n",
    "3. 实施清洗和标准化\n",
    "4. 验证清洗效果\n",
    "5. 导出清洗后数据和清洗日志\n",
    "6. 编写清洗脚本（可复用）"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python [conda env:base] *",
   "language": "python",
   "name": "conda-base-py"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
